mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-28 02:03:58 -04:00
Compare commits
292 Commits
2282-p2-ho
...
bbe1e44366
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbe1e44366 | ||
|
|
685deebae4 | ||
|
|
918016d4af | ||
|
|
f87f0a1c9e | ||
|
|
869f76c846 | ||
|
|
3946e7bb3a | ||
|
|
56ee7ec376 | ||
|
|
5dad9e8f0f | ||
|
|
23c5d7aff1 | ||
|
|
d2dd249b61 | ||
|
|
3e410ba5b2 | ||
|
|
a1802a3439 | ||
|
|
8897b30b81 | ||
|
|
b0d8eda0f4 | ||
|
|
e007571ac8 | ||
|
|
6e72bc7f5e | ||
|
|
f4bc6d4531 | ||
|
|
a5200220e2 | ||
|
|
8f45eeb2cf | ||
|
|
b134a68845 | ||
|
|
4b910bec88 | ||
|
|
238a281512 | ||
|
|
cafd7d75b7 | ||
|
|
7458acdab1 | ||
|
|
c7e3d777ac | ||
|
|
ec45db1ea4 | ||
|
|
9bed7d4253 | ||
|
|
8c56a75af7 | ||
|
|
d9eebb4f64 | ||
|
|
6546acf434 | ||
|
|
bd77775286 | ||
|
|
132ced0042 | ||
|
|
7922004f9a | ||
|
|
3a38ea2db6 | ||
|
|
3af8f782ec | ||
|
|
cbb1288345 | ||
|
|
96c033cd6f | ||
|
|
5d43ec2bb6 | ||
|
|
fff5caee1f | ||
|
|
6cfb15106e | ||
|
|
5507891d8f | ||
|
|
1a5f255539 | ||
|
|
7e2a2bad8e | ||
|
|
5c7a8913b0 | ||
|
|
52a11945a1 | ||
|
|
79ea3a3ac9 | ||
|
|
88ff982c0d | ||
|
|
77c97eeb0b | ||
|
|
5d4394bfda | ||
|
|
7485162411 | ||
|
|
d16c0c5153 | ||
|
|
5144b90f22 | ||
|
|
418a868e18 | ||
|
|
3a38b8bf31 | ||
|
|
04f4f2e990 | ||
|
|
9efe503353 | ||
|
|
012dc4fae6 | ||
|
|
35f3203e9c | ||
|
|
cb4cfeb610 | ||
|
|
15103c99b5 | ||
|
|
6dd8b3cfe0 | ||
|
|
fe098eca22 | ||
|
|
0ca31c1ed1 | ||
|
|
78ee70b0c1 | ||
|
|
71b7c745db | ||
|
|
da194d358c | ||
|
|
a539fcbef6 | ||
|
|
a5ddf8038b | ||
|
|
06d0581bc9 | ||
|
|
937aa5af15 | ||
|
|
b5203f473a | ||
|
|
dc03a3f0d1 | ||
|
|
7afdf7336a | ||
|
|
bab13de934 | ||
|
|
c05beb42f2 | ||
|
|
2049fd5c76 | ||
|
|
1d911c4d44 | ||
|
|
bc83fab764 | ||
|
|
051114157d | ||
|
|
cac7d1452a | ||
|
|
f9ea913b00 | ||
|
|
9a8a80ab14 | ||
|
|
707768d749 | ||
|
|
d05bf1e9e0 | ||
|
|
da081679c1 | ||
|
|
61c37687a2 | ||
|
|
8bc6df5d35 | ||
|
|
e84e3efed3 | ||
|
|
fdceb3cbdb | ||
|
|
26c695c21e | ||
|
|
b4907fd2fb | ||
|
|
f13ab20e93 | ||
|
|
9404a09dfa | ||
|
|
fda16b7ce9 | ||
|
|
3ae05a37c9 | ||
|
|
0df8b37646 | ||
|
|
cb334c0dcd | ||
|
|
1cd51a8257 | ||
|
|
3b8fdd8122 | ||
|
|
43c0d6244a | ||
|
|
fbf0fb1f0d | ||
|
|
965db9cd7e | ||
|
|
3555db1163 | ||
|
|
f820ae86f3 | ||
|
|
47657d45ed | ||
|
|
fab1ef6d45 | ||
|
|
10fb9f9b35 | ||
|
|
02a188a080 | ||
|
|
9facde2c60 | ||
|
|
592ea83ea2 | ||
|
|
dd0d5d3b33 | ||
|
|
29373d41ca | ||
|
|
55225018b6 | ||
|
|
5a1b656d7e | ||
|
|
46e43b6fbd | ||
|
|
542ce8ec00 | ||
|
|
371fbb2cd4 | ||
|
|
41dcedd48f | ||
|
|
d64d84ff4a | ||
|
|
558753037f | ||
|
|
821265cf6e | ||
|
|
236fafd597 | ||
|
|
22d643acb1 | ||
|
|
ff65471084 | ||
|
|
310ec3eae6 | ||
|
|
9e8aec4c5d | ||
|
|
de67e441f9 | ||
|
|
e3b35b5b83 | ||
|
|
21408543d4 | ||
|
|
aff028b24c | ||
|
|
a652c5f321 | ||
|
|
06814cd0a5 | ||
|
|
dd7d32e51c | ||
|
|
a5d66f02b5 | ||
|
|
505e5792d2 | ||
|
|
f73fe7fc9a | ||
|
|
aa3d5757f1 | ||
|
|
85ae22a81a | ||
|
|
ae619fff64 | ||
|
|
9f88a3d84f | ||
|
|
f828193a5c | ||
|
|
bc9d8969fb | ||
|
|
7ab59b9d4a | ||
|
|
b7673ae673 | ||
|
|
cee9b751bf | ||
|
|
7b39aa4761 | ||
|
|
6a224528a5 | ||
|
|
56440b4477 | ||
|
|
0a75856c2d | ||
|
|
59da570302 | ||
|
|
714473b829 | ||
|
|
ee04cae75c | ||
|
|
113f86b312 | ||
|
|
224c25a193 | ||
|
|
adf37eb4d0 | ||
|
|
e6ded78be8 | ||
|
|
f6952b28de | ||
|
|
3408cf0bd8 | ||
|
|
8f0daa19b8 | ||
|
|
59f766bb1e | ||
|
|
ec2b55c171 | ||
|
|
3fbbc4d57e | ||
|
|
32c797e10b | ||
|
|
01a940681e | ||
|
|
bd96cc1d20 | ||
|
|
71f7ed051c | ||
|
|
221f1f30fd | ||
|
|
1696614995 | ||
|
|
8415665bb1 | ||
|
|
0254dc41ca | ||
|
|
a18f981b1c | ||
|
|
cb2a7b5716 | ||
|
|
1f14c1e570 | ||
|
|
7d4e2cce25 | ||
|
|
e48a90a40c | ||
|
|
38ef9742c8 | ||
|
|
7dfb4294e1 | ||
|
|
5be95b85c1 | ||
|
|
2bec392a59 | ||
|
|
dd9cbe2f4b | ||
|
|
6fff2fb122 | ||
|
|
3f7a84b17c | ||
|
|
e59516562f | ||
|
|
9e01f3cd24 | ||
|
|
241cf8c021 | ||
|
|
2ef7f2e252 | ||
|
|
7144eaa00d | ||
|
|
ceb13a9051 | ||
|
|
e958e097aa | ||
|
|
20012885b1 | ||
|
|
b1b4611c55 | ||
|
|
9ae1f40376 | ||
|
|
3dee60fcc0 | ||
|
|
94f881477e | ||
|
|
f4ca597964 | ||
|
|
a5146fc58a | ||
|
|
0df04cfd89 | ||
|
|
34f7c6a4da | ||
|
|
cd78710753 | ||
|
|
102d005e0d | ||
|
|
3455235a8b | ||
|
|
f031f4bf18 | ||
|
|
c93d00dcbe | ||
|
|
ffda07b6bd | ||
|
|
e769d421be | ||
|
|
0daf140b39 | ||
|
|
4225559677 | ||
|
|
69457c873d | ||
|
|
06c099b567 | ||
|
|
a524ed0dc5 | ||
|
|
c37bb44e0a | ||
|
|
0b3177e3ff | ||
|
|
baf9b69fbb | ||
|
|
df3e2264ec | ||
|
|
79192e29b6 | ||
|
|
ed79352201 | ||
|
|
df4979277a | ||
|
|
becdfc79d2 | ||
|
|
951cb0fef9 | ||
|
|
2b07517e9c | ||
|
|
bb0d9a25b7 | ||
|
|
dc1bff6d19 | ||
|
|
5cede78661 | ||
|
|
5c9d190021 | ||
|
|
61fb59f1cb | ||
|
|
2bb318493a | ||
|
|
b6f341ee04 | ||
|
|
91c2ae6733 | ||
|
|
0fa8c571c8 | ||
|
|
70dae9c208 | ||
|
|
46524aa5f8 | ||
|
|
adab5f1338 | ||
|
|
02f39bd077 | ||
|
|
1eb12b93f1 | ||
|
|
f9fd580c58 | ||
|
|
fe6921e243 | ||
|
|
d20f887f0b | ||
|
|
eadd8dddb5 | ||
|
|
77248b8bf0 | ||
|
|
a8d036a3e5 | ||
|
|
05a746a042 | ||
|
|
84acce48c4 | ||
|
|
1bbc49c0ad | ||
|
|
bcd7ead5ec | ||
|
|
0405c92f69 | ||
|
|
ad1f83e80e | ||
|
|
44778349f0 | ||
|
|
5b46a4e1d7 | ||
|
|
91bea21607 | ||
|
|
ab497be1f8 | ||
|
|
5b88eefe27 | ||
|
|
34e8647fba | ||
|
|
2fdedc1b4f | ||
|
|
db1f929dba | ||
|
|
e7953262a3 | ||
|
|
89cfbd989d | ||
|
|
479cca3adb | ||
|
|
cc6a66f790 | ||
|
|
0c1dd129e0 | ||
|
|
d70a80f4a6 | ||
|
|
78cfafae94 | ||
|
|
3016aff185 | ||
|
|
4726143fb7 | ||
|
|
a4963b8263 | ||
|
|
22f729d652 | ||
|
|
38fad44a15 | ||
|
|
c1fa099c12 | ||
|
|
208891a71a | ||
|
|
afd3d94790 | ||
|
|
515419e0a2 | ||
|
|
d16eb70765 | ||
|
|
39fd87064b | ||
|
|
5c6b4132a9 | ||
|
|
23e10b1529 | ||
|
|
545bf10e0d | ||
|
|
12a2290ea9 | ||
|
|
bd619af421 | ||
|
|
2d896daa0e | ||
|
|
6ccf1bb983 | ||
|
|
5020964fb1 | ||
|
|
5f8d5996aa | ||
|
|
229842b8c9 | ||
|
|
4feace4cdc | ||
|
|
48b317ed52 | ||
|
|
709b7049c6 | ||
|
|
9630d085f4 | ||
|
|
5d565ab88b | ||
|
|
cfb966ce53 | ||
|
|
a4f6dd7100 | ||
|
|
fafc1a1bfb | ||
|
|
935c5fd62c | ||
|
|
565e6ad509 |
16
.ds.baseline
16
.ds.baseline
@@ -127,16 +127,6 @@
|
||||
}
|
||||
],
|
||||
"results": {
|
||||
".github/workflows/checks.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 68,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/assets/js/uswds.min.js": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
@@ -161,7 +151,7 @@
|
||||
"filename": "app/config.py",
|
||||
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
|
||||
"is_verified": false,
|
||||
"line_number": 120,
|
||||
"line_number": 118,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -527,7 +517,7 @@
|
||||
"filename": "tests/app/main/views/test_accept_invite.py",
|
||||
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
|
||||
"is_verified": false,
|
||||
"line_number": 643,
|
||||
"line_number": 631,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -684,5 +674,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-01-16T16:38:48Z"
|
||||
"generated_at": "2025-03-20T18:22:36Z"
|
||||
}
|
||||
|
||||
137
.github/workflows/checks.yml
vendored
137
.github/workflows/checks.yml
vendored
@@ -10,7 +10,7 @@ env:
|
||||
FLASK_APP: application.py
|
||||
WERKZEUG_DEBUG_PIN: off
|
||||
REDIS_ENABLED: 0
|
||||
NODE_VERSION: 16.15.1
|
||||
NODE_VERSION: 22.3.0
|
||||
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: Run style checks
|
||||
run: poetry run flake8 .
|
||||
- name: Check imports alphabetized
|
||||
run: poetry run isort --check-only ./app ./tests
|
||||
- name: Run style checks
|
||||
run: poetry run flake8 .
|
||||
- name: Check dead code
|
||||
run: make dead-code
|
||||
- name: Run js tests
|
||||
@@ -51,73 +51,75 @@ jobs:
|
||||
- name: Check coverage threshold
|
||||
run: poetry run coverage report --fail-under=90
|
||||
|
||||
end-to-end-tests:
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
# TODO FIX THIS!
|
||||
# end-to-end-tests:
|
||||
# if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: test_notification_api
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 6379 on service container to the host
|
||||
- 6379:6379
|
||||
# permissions:
|
||||
# checks: write
|
||||
# pull-requests: write
|
||||
# contents: write
|
||||
# runs-on: ubuntu-latest
|
||||
# environment: staging
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres
|
||||
# env:
|
||||
# POSTGRES_USER: user
|
||||
# POSTGRES_PASSWORD: password
|
||||
# POSTGRES_DB: test_notification_api
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# # Maps tcp port 5432 on service container to the host
|
||||
# - 5432:5432
|
||||
# redis:
|
||||
# image: redis
|
||||
# options: >-
|
||||
# --health-cmd "redis-cli ping"
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# # Maps tcp port 6379 on service container to the host
|
||||
# - 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- name: Check API Server availability
|
||||
run: |
|
||||
curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
- name: Run Admin server
|
||||
# If we want to log stuff and see what's broken,
|
||||
# insert this line:
|
||||
# tail -f admin-server.log &
|
||||
# above make e2e-test
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: ./.github/actions/setup-project
|
||||
# - uses: jwalton/gh-find-current-pr@v1
|
||||
# id: findPr
|
||||
# - name: Check API Server availability
|
||||
# run: |
|
||||
# curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
# - name: Run Admin server
|
||||
# # If we want to log stuff and see what's broken,
|
||||
# # insert this line:
|
||||
# # tail -f admin-server.log &
|
||||
# # above make e2e-test
|
||||
|
||||
|
||||
run: |
|
||||
make run-flask > admin-server.log 2>&1 &
|
||||
tail -f admin-server.log &
|
||||
make e2e-test
|
||||
# run: |
|
||||
# make run-flask > admin-server.log 2>&1 &
|
||||
# tail -f admin-server.log &
|
||||
# make e2e-test
|
||||
|
||||
# env:
|
||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
# SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
# DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
# ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
# ADMIN_CLIENT_USERNAME: notify-admin
|
||||
# NOTIFY_ENVIRONMENT: e2etest
|
||||
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
# NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
# VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
|
||||
env:
|
||||
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: notify-admin
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
@@ -139,7 +141,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.0.8
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
@@ -165,7 +167,6 @@ jobs:
|
||||
run: make run-flask &
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: scanning
|
||||
FEATURE_ABOUT_PAGE_ENABLED: true
|
||||
- name: Run OWASP Baseline Scan
|
||||
uses: zaproxy/action-baseline@v0.14.0
|
||||
with:
|
||||
@@ -177,7 +178,7 @@ jobs:
|
||||
cmd_options: "-I"
|
||||
|
||||
a11y-scan:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
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: 16.15.1
|
||||
NODE_VERSION: 22.3.0
|
||||
|
||||
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.0.6
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
- name: Run npm audit
|
||||
|
||||
25
.github/workflows/deploy-demo.yml
vendored
25
.github/workflows/deploy-demo.yml
vendored
@@ -16,23 +16,21 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
# 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:
|
||||
files: |
|
||||
terraform/demo/**
|
||||
terraform/shared/**
|
||||
.github/workflows/deploy-demo.yml
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/demo
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
run: terraform init
|
||||
- name: Terraform apply
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/demo
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
@@ -91,16 +89,7 @@ jobs:
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-demo.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy-demo.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
|
||||
25
.github/workflows/deploy-prod.yml
vendored
25
.github/workflows/deploy-prod.yml
vendored
@@ -16,23 +16,21 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
# 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:
|
||||
files: |
|
||||
terraform/production/**
|
||||
terraform/shared/**
|
||||
.github/workflows/deploy-prod.yml
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/production
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
run: terraform init
|
||||
- name: Terraform apply
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/production
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
@@ -91,16 +89,7 @@ jobs:
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-production.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy-prod.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
|
||||
25
.github/workflows/deploy.yml
vendored
25
.github/workflows/deploy.yml
vendored
@@ -21,23 +21,21 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
# 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:
|
||||
files: |
|
||||
terraform/staging/**
|
||||
terraform/shared/**
|
||||
.github/workflows/deploy.yml
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/staging
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
run: terraform init
|
||||
- name: Terraform apply
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/staging
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
@@ -98,16 +96,7 @@ jobs:
|
||||
--strategy rolling
|
||||
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-staging.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
|
||||
24
.github/workflows/drift.yml
vendored
24
.github/workflows/drift.yml
vendored
@@ -15,6 +15,14 @@ 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:
|
||||
@@ -35,6 +43,14 @@ 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:
|
||||
@@ -55,6 +71,14 @@ 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,6 +18,14 @@ 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
|
||||
@@ -51,7 +59,7 @@ jobs:
|
||||
|
||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||
- name: Update PR
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
# 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,6 +18,14 @@ 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
|
||||
@@ -51,7 +59,7 @@ jobs:
|
||||
|
||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||
- name: Update PR
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
# 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,6 +18,14 @@ 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
|
||||
@@ -51,7 +59,7 @@ jobs:
|
||||
|
||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||
- name: Update PR
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
# we would like to update the PR even when a prior step failed
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,6 +15,10 @@
|
||||
|
||||
## Non user files allowed to be commited
|
||||
!app/assets/pdf/tcpa_overview.pdf
|
||||
!app/assets/pdf/investing-notifications-tts-public-benefits-memo.pdf
|
||||
!app/assets/pdf/out-of-pilot-announcement.pdf
|
||||
!app/assets/pdf/studio-research-snapshot-2022-07-external.pdf
|
||||
!app/assets/pdf/TCPA-Overview.pdf
|
||||
!tests/test_pdf_files/no_eof_marker.pdf
|
||||
!tests/test_pdf_files/multi_page_pdf.pdf
|
||||
!tests/test_pdf_files/big.pdf
|
||||
|
||||
5
.pa11yci
5
.pa11yci
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"defaults": {
|
||||
"standard": "WCAG2AA",
|
||||
"runners": ["htmlcs"],
|
||||
"runners": ["htmlcs"],
|
||||
"chromeLaunchConfig": {
|
||||
"executablePath": "/usr/bin/google-chrome"
|
||||
},
|
||||
"concurrency": 1,
|
||||
"hideElements": [
|
||||
"nav > ol a",
|
||||
|
||||
12
Makefile
12
Makefile
@@ -62,6 +62,13 @@ 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 ***"
|
||||
@@ -155,3 +162,8 @@ 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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import secrets
|
||||
from functools import partial
|
||||
from time import monotonic
|
||||
@@ -168,14 +169,15 @@ def _csp(config):
|
||||
|
||||
|
||||
def create_app(application):
|
||||
@application.context_processor
|
||||
def inject_feature_flags():
|
||||
feature_about_page_enabled = application.config.get(
|
||||
"FEATURE_ABOUT_PAGE_ENABLED", False
|
||||
)
|
||||
return dict(
|
||||
FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled,
|
||||
)
|
||||
# @application.context_processor
|
||||
# def inject_feature_flags():
|
||||
# this is where feature flags can be easily added as a dictionary within context
|
||||
# feature_about_page_enabled = application.config.get(
|
||||
# "FEATURE_ABOUT_PAGE_ENABLED", False
|
||||
# )
|
||||
# return dict(
|
||||
# FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled,
|
||||
# )
|
||||
|
||||
@application.context_processor
|
||||
def inject_initial_signin_url():
|
||||
@@ -616,6 +618,8 @@ def setup_event_handlers():
|
||||
|
||||
|
||||
def add_template_filters(application):
|
||||
application.add_template_filter(slugify)
|
||||
|
||||
for fn in [
|
||||
format_auth_type,
|
||||
format_billions,
|
||||
@@ -673,3 +677,10 @@ def init_jinja(application):
|
||||
]
|
||||
jinja_loader = jinja2.FileSystemLoader(template_folders)
|
||||
application.jinja_loader = jinja_loader
|
||||
|
||||
|
||||
def slugify(text):
|
||||
"""
|
||||
Converts text to lowercase, replaces spaces with hyphens, and removes invalid characters.
|
||||
"""
|
||||
return re.sub(r"[^a-z0-9-]", "", re.sub(r"\s+", "-", text.lower()))
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{ asset_url('images/site.webmanifest') }}">
|
||||
|
||||
<link rel="stylesheet" media="screen" href="/static/stylesheets/legacy/uk.css?d077f86473501ab244de0b30600536ee" />
|
||||
<link rel="stylesheet" media="screen" href="/static/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
|
||||
<link rel="stylesheet" media="print" href="/static/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
|
||||
|
||||
<meta property="og:image" content="/static/images/notify-og-image.png">
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 474 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 362 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 292 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 373 KiB |
@@ -130,11 +130,12 @@
|
||||
.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)
|
||||
@@ -155,8 +156,10 @@
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.attr('y', d => y(d[1]))
|
||||
.attr('height', d => y(d[0]) - y(d[1]));
|
||||
};
|
||||
.attr('height', d => {
|
||||
const calculatedHeight = y(d[0]) - y(d[1]);
|
||||
return calculatedHeight < minBarHeight ? minBarHeight : calculatedHeight;
|
||||
}); };
|
||||
|
||||
// Function to create an accessible table
|
||||
const createTable = function(tableId, chartType, labels, deliveredData, failedData, pendingData) {
|
||||
@@ -213,7 +216,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var url = type === 'service' ? `/services/${currentServiceId}/daily-stats.json` : `/services/${currentServiceId}/daily-stats-by-user.json`;
|
||||
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?timezone=${encodeURIComponent(userTimezone)}`;
|
||||
|
||||
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
@@ -238,9 +247,6 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,16 +13,23 @@
|
||||
} else {
|
||||
|
||||
$submitButton.data('clicked', 'true');
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
|
||||
if ($submitButton.is('[name="Send"], [name="Schedule"]')) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000);
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let renableSubmitButton = $submitButton => () => {
|
||||
|
||||
$submitButton.data('clicked', '');
|
||||
|
||||
$submitButton.prop('disabled', false);
|
||||
};
|
||||
|
||||
$('form').on('submit', disableSubmitButtons);
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
var chartTitle = document.getElementById('chartTitle').textContent;
|
||||
|
||||
// Access data attributes from the HTML
|
||||
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;
|
||||
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
|
||||
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
|
||||
var totalMessages = messagesSent + messagesRemaining;
|
||||
|
||||
// Update the message below the chart
|
||||
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
|
||||
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
|
||||
|
||||
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
|
||||
var minSentPercentage = (sms_sent === 0) ? 0 : 0.02;
|
||||
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
|
||||
var minSentValue = totalMessages * minSentPercentage;
|
||||
var displaySent = Math.max(sms_sent, minSentValue);
|
||||
var displaySent = Math.max(messagesSent, 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: ${sms_sent.toLocaleString()}`);
|
||||
.html(`Messages Sent: ${messagesSent.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: ${sms_remaining_messages.toLocaleString()}`);
|
||||
.html(`Remaining: ${messagesRemaining.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 = sms_sent.toLocaleString(); // Value for Messages Sent
|
||||
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
|
||||
var tdRemaining = document.createElement('td');
|
||||
tdRemaining.textContent = sms_remaining_messages.toLocaleString(); // Value for Remaining
|
||||
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
|
||||
|
||||
tbodyRow.appendChild(tdMessagesSent);
|
||||
tbodyRow.appendChild(tdRemaining);
|
||||
|
||||
115
app/assets/javascripts/validation.js
Normal file
115
app/assets/javascripts/validation.js
Normal file
@@ -0,0 +1,115 @@
|
||||
function showError(input, errorElement, message) {
|
||||
errorElement.textContent = ""; // Clear existing message
|
||||
errorElement.style.display = "block";
|
||||
|
||||
// Small delay to ensure screen readers pick up the change
|
||||
setTimeout(() => {
|
||||
errorElement.textContent = message;
|
||||
}, 10);
|
||||
|
||||
if (input.type !== "radio" && input.type !== "checkbox") {
|
||||
input.classList.add("usa-input--error");
|
||||
}
|
||||
input.setAttribute("aria-describedby", errorElement.id);
|
||||
}
|
||||
|
||||
function hideError(input, errorElement) {
|
||||
errorElement.style.display = "none";
|
||||
if (input.type !== "radio" && input.type !== "checkbox") {
|
||||
input.classList.remove("usa-input--error");
|
||||
}
|
||||
input.removeAttribute("aria-describedby");
|
||||
}
|
||||
|
||||
function getFieldLabel(input) {
|
||||
const label = document.querySelector(`label[for="${input.id}"]`);
|
||||
return label ? label.textContent.trim() : "This field";
|
||||
}
|
||||
|
||||
// Attach validation logic to forms
|
||||
function attachValidation() {
|
||||
const forms = document.querySelectorAll('form[data-force-focus="True"]');
|
||||
forms.forEach((form) => {
|
||||
const inputs = form.querySelectorAll("input, textarea, select");
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
let isValid = true;
|
||||
let firstInvalidInput = null;
|
||||
const validatedRadioNames = new Set();
|
||||
|
||||
inputs.forEach((input) => {
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
let errorElement = document.getElementById(errorId);
|
||||
|
||||
if (!errorElement) {
|
||||
errorElement = document.createElement("span");
|
||||
errorElement.id = errorId;
|
||||
errorElement.classList.add("usa-error-message");
|
||||
errorElement.setAttribute("aria-live", "polite");
|
||||
errorElement.style.display = "none";
|
||||
if (input.type === "radio") {
|
||||
const group = form.querySelectorAll(`input[name="${input.name}"]`);
|
||||
const lastRadio = group[group.length - 1];
|
||||
lastRadio.parentElement.insertAdjacentElement("afterend", errorElement);
|
||||
} else {
|
||||
input.insertAdjacentElement("afterend", errorElement);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.type === "radio") {
|
||||
if (validatedRadioNames.has(input.name)) return;
|
||||
validatedRadioNames.add(input.name);
|
||||
const radioGroup = form.querySelectorAll(`input[name="${input.name}"]`);
|
||||
const isChecked = Array.from(radioGroup).some(radio => radio.checked);
|
||||
|
||||
if (!isChecked) {
|
||||
showError(input, errorElement, `Error: A selection must be made.`);
|
||||
isValid = false;
|
||||
if (!firstInvalidInput) {
|
||||
firstInvalidInput = input;
|
||||
}
|
||||
}
|
||||
} else if (input.value.trim() === "") {
|
||||
showError(input, errorElement, `Error: ${getFieldLabel(input)} is required.`);
|
||||
isValid = false;
|
||||
if (!firstInvalidInput) {
|
||||
firstInvalidInput = input;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault();
|
||||
if (firstInvalidInput) firstInvalidInput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
inputs.forEach((input) => {
|
||||
input.addEventListener("input", function () {
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
const errorElement = document.getElementById(errorId);
|
||||
if (errorElement && input.value.trim() !== "") {
|
||||
hideError(input, errorElement);
|
||||
}
|
||||
});
|
||||
if (input.type === "radio") {
|
||||
input.addEventListener("change", function () {
|
||||
const errorElement = document.getElementById(`${input.name}-error`);
|
||||
if (errorElement) {
|
||||
hideError(input, errorElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Automatically attach validation only in the browser
|
||||
if (typeof window !== "undefined") {
|
||||
document.addEventListener("DOMContentLoaded", attachValidation);
|
||||
}
|
||||
|
||||
// ✅ Check if we're in a Node.js environment (for Jest) before using `module.exports`
|
||||
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
||||
module.exports = { showError, hideError, getFieldLabel, attachValidation };
|
||||
}
|
||||
BIN
app/assets/pdf/TCPA-Overview.pdf
Normal file
BIN
app/assets/pdf/TCPA-Overview.pdf
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/assets/pdf/out-of-pilot-announcement.pdf
Normal file
BIN
app/assets/pdf/out-of-pilot-announcement.pdf
Normal file
Binary file not shown.
BIN
app/assets/pdf/studio-research-snapshot-2022-07-external.pdf
Normal file
BIN
app/assets/pdf/studio-research-snapshot-2022-07-external.pdf
Normal file
Binary file not shown.
@@ -1,186 +0,0 @@
|
||||
@use "uswds-core" as *;
|
||||
|
||||
.dashboard {
|
||||
.big-number-with-status {
|
||||
.big-number-smaller {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.big-number-number {
|
||||
font-size: units(5);
|
||||
line-height: units(5);
|
||||
}
|
||||
.big-number-label {
|
||||
font-size: units(2.5);
|
||||
}
|
||||
}
|
||||
.big-number-status {
|
||||
background: color('green-cool-40v');
|
||||
display: flex;
|
||||
padding: units(1) units(2);
|
||||
&--failing {
|
||||
padding: 0;
|
||||
a.usa-link {
|
||||
color: white;
|
||||
background: color('red-warm-50v');
|
||||
padding: units(1) units(2);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background: color('red-warm-60v');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.usa-table {
|
||||
width: 100%;
|
||||
caption {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table-field-center-aligned {
|
||||
text-align: center;
|
||||
}
|
||||
.template-statistics-table-template-name {
|
||||
padding-left: units(4);
|
||||
display: inline-block;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(../img/material-icons/description.svg);
|
||||
}
|
||||
}
|
||||
.get-started {
|
||||
border: 1px solid color('gray-90');
|
||||
padding: units(2);
|
||||
margin-bottom: units(4);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
.file-list-filename {
|
||||
font-weight: bold;
|
||||
}
|
||||
.file-list-hint {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.table-field,
|
||||
.table-field-right-aligned {
|
||||
width: 50%;
|
||||
}
|
||||
&.usage-table {
|
||||
.table-field,
|
||||
.table-field-left-aligned,
|
||||
.table-field-right-aligned {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-status-table {
|
||||
table-layout: fixed;
|
||||
|
||||
thead tr th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
th:nth-child(2),
|
||||
R td:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.usage-table {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.big-number-smallest {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.job-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
td.file-name {
|
||||
width: 25%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
td.jobid {
|
||||
width: 5%;
|
||||
}
|
||||
td.template {
|
||||
width: 25%;
|
||||
}
|
||||
td.time-sent {
|
||||
width: 15%;
|
||||
}
|
||||
td.sender {
|
||||
width: 20%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
td.count-of-recipients {
|
||||
width: 5%;
|
||||
}
|
||||
td.report {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.delivered {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.failed {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.report img {
|
||||
padding-top: 5px;
|
||||
}
|
||||
th {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.usa-table-container--scrollable-mobile {
|
||||
margin: 0;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-table th[data-sortable][aria-sort='ascending'],
|
||||
.usa-table th[data-sortable][aria-sort='descending'] {
|
||||
background-color: #a1d3ff;
|
||||
}
|
||||
|
||||
#template-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: units(1) 0 units(1) units(1);
|
||||
margin: units(2) 0 units(5);
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-prose > p.max-width-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -60,17 +60,6 @@ $failed: color('gray-cool-20');
|
||||
}
|
||||
}
|
||||
|
||||
.usa-tooltip {
|
||||
line-height: 1;
|
||||
.usa-tooltip__body {
|
||||
width: units(mobile);
|
||||
font-size: size("body", 2);
|
||||
height: auto;
|
||||
white-space: wrap;
|
||||
line-height: units(1);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 300px;
|
||||
height: 20px;
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
@use 'uswds-core' as *;
|
||||
|
||||
$do-dont-color-do: 'green-cool-50v';
|
||||
$do-dont-color-dont: 'red-cool-50v';
|
||||
$do-dont-color-border: 'gray-cool-20';
|
||||
$do-dont-padding: 3;
|
||||
$do-dont-top-bar-width: 1;
|
||||
|
||||
.do-dont {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-top: units(2);
|
||||
}
|
||||
|
||||
.do-dont__do,
|
||||
.do-dont__dont {
|
||||
background-color: color('white');
|
||||
border: 1px solid color($do-dont-color-border);
|
||||
flex: 1 0 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.do-dont__do {
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
.do-dont__do:before,
|
||||
.do-dont__dont:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: units($do-dont-top-bar-width);
|
||||
width: calc(100% + 2px);
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.do-dont__do:before {
|
||||
background-color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont:before {
|
||||
background-color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__heading {
|
||||
// @include typeset("lang", "lg", 2);
|
||||
align-items: center;
|
||||
border-bottom: 1px solid color($do-dont-color-border);
|
||||
display: flex;
|
||||
font-weight: fw('bold');
|
||||
margin: 0;
|
||||
padding: units(4) units($do-dont-padding) units($do-dont-padding);
|
||||
|
||||
.usa-icon {
|
||||
@include u-square(3);
|
||||
margin-right: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.do-dont__do .do-dont__heading {
|
||||
color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont .do-dont__heading {
|
||||
color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__content {
|
||||
padding: 0 units($do-dont-padding) units($do-dont-padding)
|
||||
units($do-dont-padding);
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: units(1);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
@use "uswds-core" as *;
|
||||
@use "tokens" as vars;
|
||||
|
||||
.usa-section--dark {
|
||||
background-color: color('gray-90');
|
||||
h1 {
|
||||
font-family: family('sans');
|
||||
font-size: units(7);
|
||||
color: vars.$notify-light-blue;
|
||||
@media (max-width: units('desktop')) {
|
||||
font-size: units(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usa-hero > .grid-container {
|
||||
@media (max-width: units('tablet')) {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-section {
|
||||
&.key-features {
|
||||
background-color: color('gray-cool-2');
|
||||
}
|
||||
&.security-and-privacy {
|
||||
img {
|
||||
filter: brightness(0) saturate(100%) invert(22%) sepia(98%) saturate(1829%) hue-rotate(187deg) brightness(88%) contrast(101%);
|
||||
}
|
||||
}
|
||||
&__home {
|
||||
h2 {
|
||||
font-size: units(6);
|
||||
line-height: 1.2;
|
||||
@media (max-width: units('desktop')) {
|
||||
font-size: units(4);
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-size: 20px;
|
||||
}
|
||||
img {
|
||||
@media (max-width: units('tablet')) {
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.home-cards {
|
||||
justify-content: space-between;
|
||||
.usa-card__container {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
.img-container {
|
||||
width: units('card');
|
||||
height: units('card');
|
||||
margin: units(3) 0;
|
||||
border-radius: 50%;
|
||||
background: color('gray-cool-10');
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
img {
|
||||
width: units(10);
|
||||
height: units(10);
|
||||
filter: brightness(0) saturate(100%) invert(22%) sepia(98%) saturate(1829%) hue-rotate(187deg) brightness(88%) contrast(101%);
|
||||
}
|
||||
}
|
||||
h3, p {
|
||||
font-size: units(3);
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
@use "uswds-core" as *;
|
||||
|
||||
// Tabs
|
||||
|
||||
.tabs {
|
||||
.pill {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
.pill-item__container {
|
||||
border: 1px solid color('gray-cool-10');
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
font-size: units(2);
|
||||
a {
|
||||
padding: units(4);
|
||||
.big-number-smaller {
|
||||
font-size: units(5);
|
||||
line-height: units(6);
|
||||
}
|
||||
.big-number-smallest {
|
||||
font-size: units(3);
|
||||
}
|
||||
&:not(.pill-item--selected):hover {
|
||||
background: color('blue-warm-70v');
|
||||
}
|
||||
&.pill-item--selected:hover {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
@use "uswds-core" as *;
|
||||
|
||||
// Colors
|
||||
$notify-primary: color('blue-60v');
|
||||
$notify-secondary: color('red-warm-50v');
|
||||
$notify-light-blue: color('blue-40v');
|
||||
$notify-tertiary: color('gold-20v');
|
||||
$notify-text: color('ink');
|
||||
@@ -1,3 +1,25 @@
|
||||
/*
|
||||
* * * * * ==============================
|
||||
* * * * * ==============================
|
||||
* * * * * ==============================
|
||||
* * * * * ==============================
|
||||
========================================
|
||||
========================================
|
||||
========================================
|
||||
----------------------------------------
|
||||
USWDS THEME CUSTOM STYLES
|
||||
----------------------------------------
|
||||
!! Copy this file to your project's
|
||||
sass root. Don't edit the version
|
||||
in node_modules.
|
||||
----------------------------------------
|
||||
Custom project SASS goes here.
|
||||
|
||||
i.e.
|
||||
@include u-padding-right('05');
|
||||
----------------------------------------
|
||||
*/
|
||||
|
||||
@use 'uswds-core' as *;
|
||||
|
||||
iframe:focus,
|
||||
@@ -34,10 +56,7 @@ button:not([disabled]):focus {
|
||||
}
|
||||
}
|
||||
@include at-media-max('desktop') {
|
||||
.usa-nav__secondary-links
|
||||
{
|
||||
padding-left: units(2);
|
||||
}
|
||||
padding: 0 units(2);
|
||||
ul li {
|
||||
padding-bottom: units(1);
|
||||
}
|
||||
@@ -120,11 +139,6 @@ td.table-empty-message {
|
||||
font-family: family('sans');
|
||||
}
|
||||
|
||||
.usa-dark-background .pill-item__label {
|
||||
color: white;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.pill-item.usa-link {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
@@ -141,17 +155,9 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin-right: 0;
|
||||
img {
|
||||
height: 1rem;
|
||||
}
|
||||
@media (max-width: units('desktop')) {
|
||||
img {
|
||||
height: 14px;
|
||||
}
|
||||
font-size: 14px;
|
||||
}
|
||||
.usa-button img {
|
||||
margin-left: 0.5rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.usa-button.login-button.login-button--primary,
|
||||
@@ -187,9 +193,6 @@ td.table-empty-message {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
// border: 1px solid color('gray-cool-10');
|
||||
// padding: units(2);
|
||||
|
||||
.tick-cross-list-permissions {
|
||||
margin: units(1) 0;
|
||||
padding-left: units(2);
|
||||
@@ -246,6 +249,11 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.usa-checkbox.template-list-item.template-list-item-with-checkbox.template-list-item-without-ancestors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usa-checkbox__label-description {
|
||||
margin: units(2px) 0 units(1) units(4);
|
||||
}
|
||||
@@ -277,7 +285,8 @@ td.table-empty-message {
|
||||
@include u-width('mobile-lg');
|
||||
margin-top: units(2);
|
||||
}
|
||||
input#search {
|
||||
input#search-by-name {
|
||||
margin-top: units(1);
|
||||
width: 100%;
|
||||
border: 1px solid color('gray-60');
|
||||
}
|
||||
@@ -354,7 +363,258 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
// Etc
|
||||
// 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%;
|
||||
}
|
||||
|
||||
.usa-tooltip {
|
||||
&__information {
|
||||
background: color('ink');
|
||||
margin-top: 4px;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
height: units(3);
|
||||
width: units(3);
|
||||
border: 1px solid color('ink');
|
||||
}
|
||||
.usa-tooltip__body {
|
||||
min-width: units('card-lg');
|
||||
max-width: units('mobile');
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
.pill-item__label {
|
||||
color: white;
|
||||
}
|
||||
&:not(.pill-item--selected):hover {
|
||||
background: color('blue-warm-70v');
|
||||
|
||||
.pill-item__label {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
&.pill-item--selected {
|
||||
.pill-item__label {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
&:hover {
|
||||
.pill-item__label {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-brand,
|
||||
.browse-list {
|
||||
@@ -515,6 +775,86 @@ div.guides {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
$do-dont-color-do: 'green-cool-50v';
|
||||
$do-dont-color-dont: 'red-cool-50v';
|
||||
$do-dont-color-border: 'gray-cool-20';
|
||||
$do-dont-padding: 3;
|
||||
$do-dont-top-bar-width: 1;
|
||||
|
||||
.do-dont {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-top: units(2);
|
||||
}
|
||||
|
||||
.do-dont__do,
|
||||
.do-dont__dont {
|
||||
background-color: color('white');
|
||||
border: 1px solid color($do-dont-color-border);
|
||||
flex: 1 0 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.do-dont__do {
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
.do-dont__do:before,
|
||||
.do-dont__dont:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: units($do-dont-top-bar-width);
|
||||
width: calc(100% + 2px);
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.do-dont__do:before {
|
||||
background-color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont:before {
|
||||
background-color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__heading {
|
||||
// @include typeset("lang", "lg", 2);
|
||||
align-items: center;
|
||||
border-bottom: 1px solid color($do-dont-color-border);
|
||||
display: flex;
|
||||
font-weight: fw('bold');
|
||||
margin: 0;
|
||||
padding: units(4) units($do-dont-padding) units($do-dont-padding);
|
||||
|
||||
.usa-icon {
|
||||
@include u-square(3);
|
||||
margin-right: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.do-dont__do .do-dont__heading {
|
||||
color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont .do-dont__heading {
|
||||
color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__content {
|
||||
padding: 0 units($do-dont-padding) units($do-dont-padding)
|
||||
units($do-dont-padding);
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: units(1);
|
||||
}
|
||||
}
|
||||
|
||||
.site-note {
|
||||
background-color: #dfe1e2;
|
||||
max-width: 72ex;
|
||||
@@ -659,6 +999,32 @@ nav.nav {
|
||||
}
|
||||
}
|
||||
|
||||
.home-cards {
|
||||
justify-content: space-between;
|
||||
.usa-card__container {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
img {
|
||||
margin: units(4) auto 0;
|
||||
width: units(15);
|
||||
height: units(15);
|
||||
}
|
||||
.usa-card__body {
|
||||
margin-bottom: units(2);
|
||||
}
|
||||
.blue-bar {
|
||||
background-color: #005eb8;
|
||||
height: 1.3em;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.contact-us-card {
|
||||
border: 2px solid color("ink");
|
||||
@@ -690,3 +1056,7 @@ nav.nav {
|
||||
.form-control-error {
|
||||
border: 4px solid #b10e1e
|
||||
}
|
||||
|
||||
.usa-site-alert .usa-alert .usa-alert__body {
|
||||
max-width: 75rem;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
@forward "uswds-theme";
|
||||
@forward "uswds";
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@use "tokens" as vars;
|
||||
@forward "legacy-styles";
|
||||
@forward "home";
|
||||
@forward "dashboard";
|
||||
@forward "tabs";
|
||||
@forward "do-dont";
|
||||
@forward "data-visualization";
|
||||
|
||||
90
app/assets/stylesheets/main.scss
Normal file
90
app/assets/stylesheets/main.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
// Path to assets for use with file-url()
|
||||
$path: '/static/images/';
|
||||
|
||||
// Dependencies from GOV.UK Frontend Toolkit
|
||||
// https://github.com/alphagov/govuk_frontend_toolkit/
|
||||
@import 'conditionals';
|
||||
@import 'shims';
|
||||
@import 'measurements';
|
||||
@import 'css3';
|
||||
@import 'colours';
|
||||
@import 'typography';
|
||||
@import 'grid_layout';
|
||||
@import 'helpers';
|
||||
|
||||
// Dependencies from GOVU.UK Frontend Toolkit, rewritten for this application
|
||||
@import 'url-helpers';
|
||||
|
||||
// Specific to this application, needs to go at the top of the cascade
|
||||
@import 'globals';
|
||||
|
||||
// Dependencies from GOV.UK Elements
|
||||
// https://github.com/alphagov/govuk_elements
|
||||
@import 'elements/helpers';
|
||||
@import 'elements/reset';
|
||||
@import 'elements/details';
|
||||
@import 'elements/elements-typography';
|
||||
@import 'elements/forms';
|
||||
@import 'elements/forms/form-multiple-choice';
|
||||
@import 'elements/forms/form-validation';
|
||||
@import 'elements/lists';
|
||||
@import 'elements/panels';
|
||||
@import 'elements/tables';
|
||||
|
||||
// Dependencies from GOV.UK Frontend, packaged to be specific to this application
|
||||
@import './govuk-frontend/all';
|
||||
|
||||
// Custom overrides
|
||||
.govuk-link {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Specific to this application
|
||||
@import 'local/typography';
|
||||
@import 'grids';
|
||||
@import 'components/site-footer';
|
||||
@import 'components/placeholder';
|
||||
@import 'components/sms-message';
|
||||
@import 'components/page-footer';
|
||||
@import 'components/table';
|
||||
@import 'components/navigation';
|
||||
@import 'components/big-number';
|
||||
@import 'components/banner';
|
||||
@import 'components/textbox';
|
||||
@import 'components/file-upload';
|
||||
@import 'components/browse-list';
|
||||
@import 'components/email-message';
|
||||
@import 'components/copy-to-clipboard';
|
||||
@import 'components/vendor/previous-next-navigation';
|
||||
@import 'components/radios';
|
||||
@import 'components/checkboxes';
|
||||
@import 'components/pill';
|
||||
@import 'components/show-more';
|
||||
@import 'components/message';
|
||||
@import 'components/research-mode';
|
||||
@import 'components/tick-cross';
|
||||
@import 'components/list-entry';
|
||||
@import 'components/live-search';
|
||||
@import 'components/stick-at-top-when-scrolling';
|
||||
@import 'components/fullscreen-table';
|
||||
@import 'components/conditional-radios';
|
||||
@import 'components/vendor/breadcrumbs';
|
||||
@import 'components/vendor/responsive-embed';
|
||||
@import 'components/preview-pane';
|
||||
@import 'components/task-list';
|
||||
@import 'components/loading-indicator';
|
||||
@import 'components/area-list';
|
||||
@import 'components/content-metadata';
|
||||
|
||||
@import 'views/dashboard';
|
||||
@import 'views/users';
|
||||
@import 'views/api';
|
||||
@import 'views/product-page';
|
||||
@import 'views/template';
|
||||
@import 'views/notification';
|
||||
@import 'views/send';
|
||||
@import 'views/get_started';
|
||||
@import 'views/history';
|
||||
|
||||
// TODO: break this up
|
||||
@import 'app';
|
||||
@@ -88,8 +88,6 @@ class Config(object):
|
||||
],
|
||||
}
|
||||
|
||||
FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true"
|
||||
|
||||
|
||||
def _s3_credentials_from_env(bucket_prefix):
|
||||
return {
|
||||
|
||||
@@ -61,7 +61,7 @@ class ValidEmail:
|
||||
|
||||
|
||||
class NoCommasInPlaceHolders:
|
||||
def __init__(self, message="You cannot put commas between double brackets"):
|
||||
def __init__(self, message="You cannot put commas between double parenthesis"):
|
||||
self.message = message
|
||||
|
||||
def __call__(self, form, field):
|
||||
@@ -108,33 +108,17 @@ class OnlySMSCharacters:
|
||||
)
|
||||
if non_sms_characters:
|
||||
raise ValidationError(
|
||||
"You cannot use {} in {}. {} will not show up properly on everyone’s phones.".format(
|
||||
"Please remove the unaccepted character {} in your message, then save again".format(
|
||||
formatted_list(
|
||||
non_sms_characters,
|
||||
conjunction="or",
|
||||
conjunction="and",
|
||||
before_each="",
|
||||
after_each="",
|
||||
),
|
||||
{
|
||||
"sms": "text messages",
|
||||
}.get(self._template_type),
|
||||
("It" if len(non_sms_characters) == 1 else "They"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# class NoPlaceholders:
|
||||
|
||||
# def __init__(self, message=None):
|
||||
# self.message = message or (
|
||||
# 'You can’t use ((double brackets)) to personalize this message'
|
||||
# )
|
||||
|
||||
# def __call__(self, form, field):
|
||||
# if Field(field.data).placeholders:
|
||||
# raise ValidationError(self.message)
|
||||
|
||||
|
||||
class LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly:
|
||||
regex = re.compile(r"^[a-zA-Z0-9\s\._']+$")
|
||||
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
import calendar
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
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 import abort, jsonify, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from app import (
|
||||
billing_api_client,
|
||||
current_service,
|
||||
job_api_client,
|
||||
service_api_client,
|
||||
template_statistics_client,
|
||||
)
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric
|
||||
from app.main import main
|
||||
from app.main.views.user_profile import set_timezone
|
||||
from app.statistics_utils import get_formatted_percentage
|
||||
from app.utils import (
|
||||
DELIVERED_STATUSES,
|
||||
FAILURE_STATUSES,
|
||||
REQUESTED_STATUSES,
|
||||
service_has_permission,
|
||||
)
|
||||
from app.utils.csv import Spreadsheet
|
||||
from app.utils.pagination import generate_next_dict, generate_previous_dict
|
||||
from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, REQUESTED_STATUSES
|
||||
from app.utils.time import get_current_financial_year
|
||||
from app.utils.user import user_has_permissions
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard")
|
||||
@@ -48,68 +39,111 @@ 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
|
||||
|
||||
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)
|
||||
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
|
||||
]
|
||||
|
||||
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)
|
||||
all_statistics = template_statistics_client.get_template_statistics_for_service(
|
||||
service_id, limit_days=7
|
||||
)
|
||||
template_statistics = aggregate_template_usage(all_statistics)
|
||||
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=sorted_jobs,
|
||||
jobs=job_lists,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
sms_sent=sms_sent,
|
||||
sms_allowance_remaining=sms_allowance_remaining,
|
||||
messages_remaining=messages_remaining,
|
||||
messages_sent=messages_sent,
|
||||
template_statistics=template_statistics,
|
||||
most_used_template_count=max(
|
||||
[row["count"] for row in template_statistics] or [0]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def job_is_finished(job_dict):
|
||||
done_statuses = DELIVERED_STATUSES + FAILURE_STATUSES + ["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()
|
||||
stats = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||
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,
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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(
|
||||
days = date_range["days"]
|
||||
user_timezone = request.args.get("timezone", "UTC")
|
||||
|
||||
stats_utc = service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
user_id=current_user.id,
|
||||
start_date=date_range["start_date"],
|
||||
days=date_range["days"],
|
||||
days=days,
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard.json")
|
||||
@user_has_permissions("view_activity")
|
||||
def service_dashboard_updates(service_id):
|
||||
return jsonify(**get_dashboard_partials(service_id))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/template-activity")
|
||||
@user_has_permissions("view_activity")
|
||||
def template_history(service_id):
|
||||
return redirect(url_for("main.template_usage", service_id=service_id), code=301)
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
|
||||
return jsonify(local_stats)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/template-usage")
|
||||
@@ -197,103 +231,6 @@ def usage(service_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/monthly")
|
||||
@user_has_permissions("view_activity")
|
||||
def monthly(service_id):
|
||||
year, current_financial_year = requested_and_current_financial_year(request)
|
||||
return render_template(
|
||||
"views/dashboard/monthly.html",
|
||||
months=format_monthly_stats_to_list(
|
||||
service_api_client.get_monthly_notification_stats(service_id, year)["data"]
|
||||
),
|
||||
years=get_tuples_of_financial_years(
|
||||
partial_url=partial(url_for, ".monthly", service_id=service_id),
|
||||
start=current_financial_year - 2,
|
||||
end=current_financial_year,
|
||||
),
|
||||
selected_year=year,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/inbox")
|
||||
@user_has_permissions("view_activity")
|
||||
@service_has_permission("inbound_sms")
|
||||
def inbox(service_id):
|
||||
return render_template(
|
||||
"views/dashboard/inbox.html",
|
||||
partials=get_inbox_partials(service_id),
|
||||
updates_url=url_for(
|
||||
".inbox_updates", service_id=service_id, page=request.args.get("page")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/inbox.json")
|
||||
@user_has_permissions("view_activity")
|
||||
@service_has_permission("inbound_sms")
|
||||
def inbox_updates(service_id):
|
||||
return jsonify(get_inbox_partials(service_id))
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/inbox.csv")
|
||||
@user_has_permissions("view_activity")
|
||||
def inbox_download(service_id):
|
||||
return Response(
|
||||
Spreadsheet.from_rows(
|
||||
[
|
||||
[
|
||||
"Phone number",
|
||||
"Message",
|
||||
"Received",
|
||||
]
|
||||
]
|
||||
+ [
|
||||
[
|
||||
format_phone_number_human_readable(message["user_number"]),
|
||||
message["content"].lstrip(("=+-@")),
|
||||
format_datetime_numeric(message["created_at"]),
|
||||
]
|
||||
for message in service_api_client.get_inbound_sms(service_id)["data"]
|
||||
]
|
||||
).as_csv_data,
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": 'inline; filename="Received text messages {}.csv"'.format(
|
||||
format_date_numeric(datetime.utcnow().isoformat())
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_inbox_partials(service_id):
|
||||
page = int(request.args.get("page", 1))
|
||||
inbound_messages_data = service_api_client.get_most_recent_inbound_sms(
|
||||
service_id, page=page
|
||||
)
|
||||
inbound_messages = inbound_messages_data["data"]
|
||||
if not inbound_messages:
|
||||
inbound_number = current_service.inbound_number
|
||||
else:
|
||||
inbound_number = None
|
||||
|
||||
prev_page = None
|
||||
if page > 1:
|
||||
prev_page = generate_previous_dict("main.inbox", service_id, page)
|
||||
next_page = None
|
||||
if inbound_messages_data["has_next"]:
|
||||
next_page = generate_next_dict("main.inbox", service_id, page)
|
||||
|
||||
return {
|
||||
"messages": render_template(
|
||||
"views/dashboard/_inbox_messages.html",
|
||||
messages=inbound_messages,
|
||||
inbound_number=inbound_number,
|
||||
prev_page=prev_page,
|
||||
next_page=next_page,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def filter_out_cancelled_stats(template_statistics):
|
||||
return [s for s in template_statistics if s["status"] != "cancelled"]
|
||||
|
||||
@@ -325,71 +262,6 @@ def aggregate_template_usage(template_statistics, sort_key="count"):
|
||||
return sorted(templates, key=lambda x: x[sort_key], reverse=True)
|
||||
|
||||
|
||||
def aggregate_notifications_stats(template_statistics):
|
||||
template_statistics = filter_out_cancelled_stats(template_statistics)
|
||||
notifications = {
|
||||
template_type: {status: 0 for status in ("requested", "delivered", "failed")}
|
||||
for template_type in ["sms", "email"]
|
||||
}
|
||||
for stat in template_statistics:
|
||||
notifications[stat["template_type"]]["requested"] += stat["count"]
|
||||
if stat["status"] in DELIVERED_STATUSES:
|
||||
notifications[stat["template_type"]]["delivered"] += stat["count"]
|
||||
elif stat["status"] in FAILURE_STATUSES:
|
||||
notifications[stat["template_type"]]["failed"] += stat["count"]
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
def get_dashboard_partials(service_id):
|
||||
all_statistics = template_statistics_client.get_template_statistics_for_service(
|
||||
service_id, limit_days=7
|
||||
)
|
||||
template_statistics = aggregate_template_usage(all_statistics)
|
||||
stats = aggregate_notifications_stats(all_statistics)
|
||||
|
||||
dashboard_totals = (get_dashboard_totals(stats),)
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
current_service.id,
|
||||
)
|
||||
# These 2 calls will update the dashboard sms allowance count while in trial mode.
|
||||
billing_api_client.get_monthly_usage_for_service(
|
||||
service_id, get_current_financial_year()
|
||||
)
|
||||
billing_api_client.create_or_update_free_sms_fragment_limit(
|
||||
service_id, free_sms_fragment_limit=free_sms_allowance
|
||||
)
|
||||
|
||||
yearly_usage = billing_api_client.get_annual_usage_for_service(
|
||||
service_id,
|
||||
get_current_financial_year(),
|
||||
)
|
||||
return {
|
||||
"upcoming": render_template(
|
||||
"views/dashboard/_upcoming.html",
|
||||
),
|
||||
"inbox": render_template(
|
||||
"views/dashboard/_inbox.html",
|
||||
),
|
||||
"totals": render_template(
|
||||
"views/dashboard/_totals.html",
|
||||
service_id=service_id,
|
||||
statistics=dashboard_totals[0],
|
||||
),
|
||||
"template-statistics": render_template(
|
||||
"views/dashboard/template-statistics.html",
|
||||
template_statistics=template_statistics,
|
||||
most_used_template_count=max(
|
||||
[row["count"] for row in template_statistics] or [0]
|
||||
),
|
||||
),
|
||||
"usage": render_template(
|
||||
"views/dashboard/_usage.html",
|
||||
**get_annual_usage_breakdown(yearly_usage, free_sms_allowance),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_dashboard_totals(statistics):
|
||||
|
||||
for msg_type in statistics.values():
|
||||
|
||||
@@ -23,10 +23,11 @@ from app.utils.user import user_is_logged_in
|
||||
# Hook to check for feature flags
|
||||
@main.before_request
|
||||
def check_feature_flags():
|
||||
if request.path.startswith("/about") and not current_app.config.get(
|
||||
"FEATURE_ABOUT_PAGE_ENABLED", False
|
||||
):
|
||||
abort(404)
|
||||
# Placeholder for future feature flag checks
|
||||
# Example:
|
||||
# if request.path.startswith("/some-feature") and not current_app.config.get("FEATURE_SOME_FEATURE_ENABLED", False):
|
||||
# abort(404)
|
||||
pass
|
||||
|
||||
|
||||
@main.route("/test/feature-flags")
|
||||
@@ -258,11 +259,11 @@ def why_text_messaging():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/join-notify")
|
||||
def join_notify():
|
||||
@main.route("/notify-service-ending")
|
||||
@user_is_logged_in
|
||||
def notify_service_ending():
|
||||
return render_template(
|
||||
"views/join-notify.html",
|
||||
navigation_links=about_notify_nav(),
|
||||
"views/notify-service-ending.html",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -116,6 +116,70 @@ 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
|
||||
|
||||
@@ -167,7 +167,7 @@ def send_messages(service_id, template_id):
|
||||
# just show the first error, as we don't expect the form to have more
|
||||
# than one, since it only has one field
|
||||
first_field_errors = list(form.errors.values())[0]
|
||||
error_message = '<span class="usa-error-message">'
|
||||
error_message = '<span class="error-message usa-error-message">'
|
||||
error_message = f"{error_message}{first_field_errors[0]}"
|
||||
error_message = f"{error_message}</span>"
|
||||
error_message = Markup(error_message)
|
||||
@@ -924,6 +924,10 @@ def get_template_error_dict(exception):
|
||||
def preview_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
if not recipient:
|
||||
current_app.logger.warning(
|
||||
f"No recipient found for service {service_id}, template {template_id}. Redirecting..."
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".send_one_off",
|
||||
|
||||
@@ -68,11 +68,12 @@ 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)}"
|
||||
stored_nonce = redis_client.get(nonce_key).decode("utf8")
|
||||
if not os.getenv("NOTIFY_ENVIRONMENT") == "development":
|
||||
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"]
|
||||
@@ -112,7 +113,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:
|
||||
if not verify_path and not os.getenv("NOTIFY_ENVIRONMENT") == "development":
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
||||
if state != stored_state:
|
||||
|
||||
@@ -56,10 +56,6 @@ def about_notify_nav():
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Join Notify",
|
||||
"link": "main.join_notify",
|
||||
},
|
||||
{
|
||||
"name": "Contact us",
|
||||
"link": "main.contact",
|
||||
|
||||
@@ -680,41 +680,45 @@ def count_content_length(service_id, template_type):
|
||||
)
|
||||
|
||||
|
||||
def _is_latin1(s):
|
||||
return bool(s.encode(encoding="latin-1", errors="strict"))
|
||||
|
||||
|
||||
def _get_content_count_error_and_message_for_template(template):
|
||||
url = "https://en.wikipedia.org/wiki/ISO/IEC_8859-1"
|
||||
if template.template_type == "sms":
|
||||
s1 = f"<html><body>Use of characters outside the <a href='{url}'>IEC_8859-1</a> character set may increase "
|
||||
s2 = "the message fragment count, resulting in additional charges, and these IEC_8859-1 "
|
||||
s3 = "characters may not display properly on some older mobile devices.</body></html>"
|
||||
s1 = (
|
||||
"<html><body>Looks like your template may have one of these characters "
|
||||
"• ™ ∞ ≤ or ≥ or emoji, which won't save."
|
||||
)
|
||||
s2 = "<br>Please remove any unaccepted characters or emojis and try again.</body></html>"
|
||||
|
||||
warning = ""
|
||||
try:
|
||||
_is_latin1(template.content)
|
||||
except UnicodeEncodeError:
|
||||
warning = f"{s1}{s2}{s3}"
|
||||
# Define characters that should be blocked
|
||||
BLOCKED_CHARACTERS = {"•", "™", "∞", "≤", "≥"}
|
||||
|
||||
def contains_blocked_characters(content):
|
||||
"""Check if the content contains explicitly blocked characters."""
|
||||
return any(c in BLOCKED_CHARACTERS for c in content)
|
||||
|
||||
# Check for blocked characters
|
||||
if contains_blocked_characters(template.content):
|
||||
warning = f"{s1}{s2}"
|
||||
return False, Markup(
|
||||
warning
|
||||
) # 🚨 ONLY show the warning, hiding "Will be charged..."
|
||||
|
||||
# If message is too long, return the length error
|
||||
if template.is_message_too_long():
|
||||
return True, (
|
||||
f"You have "
|
||||
f"{character_count(template.content_count_without_prefix - SMS_CHAR_COUNT_LIMIT)} "
|
||||
f"too many"
|
||||
)
|
||||
|
||||
# Show charge message as usual if no warning
|
||||
if template.placeholders:
|
||||
return False, (
|
||||
Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
f"(not including personalization). {warning}"
|
||||
)
|
||||
)
|
||||
return False, (
|
||||
# Markup marks html contents safe so that they render properly. Don't use it if there is user input.
|
||||
Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)}. {warning} "
|
||||
return False, Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
f"(not including personalization)."
|
||||
)
|
||||
|
||||
return False, Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)}."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -58,8 +58,6 @@ class HeaderNavigation(Navigation):
|
||||
},
|
||||
"accounts-or-dashboard": {
|
||||
"conversation",
|
||||
"inbox",
|
||||
"monthly",
|
||||
"service_dashboard",
|
||||
"template_usage",
|
||||
"view_notification",
|
||||
@@ -161,8 +159,6 @@ class MainNavigation(Navigation):
|
||||
},
|
||||
"dashboard": {
|
||||
"conversation",
|
||||
"inbox",
|
||||
"monthly",
|
||||
"service_dashboard",
|
||||
"template_usage",
|
||||
"view_notification",
|
||||
|
||||
@@ -537,6 +537,11 @@ 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()
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
data["next"] = next_string
|
||||
if code_type == "email":
|
||||
data["email_auth_link_host"] = self.admin_url
|
||||
endpoint = f"/user/{user_id}/{code_type}-code"
|
||||
endpoint = f"/user/{user_id}/{code_type}-code"
|
||||
current_app.logger.warn(hilite(f"Sending verify_code {code_type} to {user_id}"))
|
||||
self.post(endpoint, data=data)
|
||||
|
||||
|
||||
@@ -14,10 +14,8 @@
|
||||
<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 %}
|
||||
|
||||
@@ -31,6 +29,30 @@
|
||||
|
||||
{% block header %}
|
||||
{% include 'components/usa_banner.html' %}
|
||||
{% if current_user.is_authenticated or current_service or current_user.platform_admin %}
|
||||
<section class="usa-site-alert usa-site-alert--info" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
GSA will no longer offer the Notify.gov service after June 8th, 2025. Visit
|
||||
<a class="usa-link" href="/notify-service-ending">Notify.gov Service Ending</a> for more information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-heading" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
Notify.gov is no longer accepting new partners.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% include 'components/header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -96,10 +118,10 @@
|
||||
{% block footer %}
|
||||
|
||||
{% if current_service and current_service.research_mode %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a><span id="research-mode" class="research-mode">research mode</span>' %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://tts.gsa.gov/" class="usa-link">Technology Transformation Services</a><span id="research-mode" class="research-mode">research mode</span>' %}
|
||||
{% else %}
|
||||
{% set commit_hash = ", Latest version: " + config['COMMIT_HASH'] %}
|
||||
{% set long_link = '<a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set long_link = '<a href="https://tts.gsa.gov/" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||
{% endif %}
|
||||
|
||||
@@ -145,10 +167,8 @@
|
||||
{% 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>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{#- Define common attributes we can use for both button and input types #}
|
||||
|
||||
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
||||
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name | trim }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
||||
|
||||
{#- Actually create a button... or a link! #}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
classes: params.label.classes,
|
||||
isPageHeading: params.label.isPageHeading,
|
||||
attributes: params.label.attributes,
|
||||
for: params.id
|
||||
for: params.text
|
||||
}) | indent(2) | trim }}
|
||||
{% if params.hint %}
|
||||
{% set hintId = params.id + '-hint' %}
|
||||
@@ -26,7 +26,7 @@
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
{% if params.errorMessage %}
|
||||
{% set errorId = params.id + '-error' %}
|
||||
{% set errorId = params.label.text + '-error' %}
|
||||
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
||||
{{ usaErrorMessage({
|
||||
id: errorId,
|
||||
@@ -37,12 +37,16 @@
|
||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
<input class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}" id="{{ params.id }}" name="{{ params.name }}" type="{{ params.type | default('text') }}"
|
||||
{%- if params.value %} value="{{ params.value}}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete}}"{% endif %}
|
||||
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}
|
||||
{%- if params.required %} required{% endif %}
|
||||
<input
|
||||
class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}"
|
||||
id="{{ params.label.text | default('unknown') | slugify }}"
|
||||
name="{{ params.name }}"
|
||||
type="{{ params.type | default('text') }}"
|
||||
{%- if params.value %} value="{{ params.value }}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete }}"{% endif %}
|
||||
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}
|
||||
{%- if params.required %} required{% endif %}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if field.errors and show_errors %}
|
||||
<span class="error-message">
|
||||
<span class="error-message usa-error-message">
|
||||
{{ field.errors[0] }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
class=None,
|
||||
id=None,
|
||||
module=None,
|
||||
data_kwargs={}
|
||||
data_kwargs={},
|
||||
data_force_focus=False
|
||||
) %}
|
||||
<form
|
||||
method="{{ method }}"
|
||||
@@ -19,6 +20,7 @@
|
||||
data-{{ key }}="{{ val }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data_force_focus %}data-force-focus="{{ data_force_focus }}"{% endif %}
|
||||
novalidate
|
||||
>
|
||||
{{ caller() }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% set is_about_page = request.path.startswith('/about') %}
|
||||
{% set is_join_notify_page = request.path.startswith('/join-notify') %}
|
||||
{% set is_contact_page = request.path.startswith('/contact') %}
|
||||
{% set is_information_section = is_about_page or is_join_notify_page or is_contact_page %}
|
||||
{% set is_information_section = is_about_page or is_contact_page %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set navigation = [
|
||||
@@ -34,11 +33,16 @@
|
||||
{% else %}
|
||||
{% set navigation = [
|
||||
{"href": url_for('main.about_notify'), "text": "About Notify", "active": is_about_page},
|
||||
{"href": url_for('main.join_notify'), "text": "Join Notify", "active": is_join_notify_page},
|
||||
{"href": url_for('main.contact'), "text": "Contact us", "active": is_contact_page}
|
||||
] %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
@@ -47,7 +51,7 @@
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.svg' }}" alt=""
|
||||
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.svg' }}" alt="Notify.gov logo"
|
||||
class="usa-flag-logo margin-right-1">
|
||||
</a>
|
||||
</div>
|
||||
@@ -55,6 +59,14 @@
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not current_user.is_authenticated and is_information_section%}
|
||||
<div class="usa-nav__login">
|
||||
<a class="usa-button usa-button login-button login-button--primary margin-right-2"
|
||||
href="{{ initial_signin_url }}">Sign
|
||||
in with <img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -75,11 +87,7 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<div class="usa-nav__secondary margin-bottom-2">
|
||||
{% else %}
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
{% endif %}
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% if secondaryNavigation %}
|
||||
{% for item in secondaryNavigation %}
|
||||
@@ -94,19 +102,8 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<div class="usa-nav__login display-flex flex-align-center">
|
||||
<span class="margin-right-2 display-none desktop:display-block">
|
||||
If you are an existing partner
|
||||
</span>
|
||||
<a id="header_login_button" class="usa-button login-button login-button--primary desktop:padding-2 padding-1"
|
||||
href="{{ initial_signin_url }}">
|
||||
Sign in with
|
||||
<img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<legend class="form-label {% if bold_legend %}bold{% endif %}">
|
||||
{{ field.label.text }}
|
||||
{% if field.errors %}
|
||||
<span class="error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||
<span class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||
{{ field.errors[0] }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<span class="error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||
<span class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||
{{ field.errors[0] }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<span id="{{ field.name}}-error" class="usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
|
||||
<span id="{{ field.name}}-error" class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
|
||||
<span class="usa-sr-only">Error:</span>
|
||||
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
|
||||
</span>
|
||||
|
||||
@@ -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 been scheduled</h2>
|
||||
<h2 class="usa-alert__heading">Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} 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 sending
|
||||
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} 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 pending
|
||||
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} 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 been sent
|
||||
Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} been sent
|
||||
</h2>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }}
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
</h3>
|
||||
<div id="message-length" class="usa-accordion__content usa-prose">
|
||||
<p class="usa-body">
|
||||
If your message is long then it will
|
||||
cost more.
|
||||
</p>
|
||||
<p class="usa-body">
|
||||
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a> for details.
|
||||
Each message sent to a recipient counts as one text. Keep track on your Dashboard of how many messages have sent and are remaining.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</h3>
|
||||
<div id="m-a2" class="usa-accordion__content usa-prose">
|
||||
<p class="usa-body">
|
||||
Use double brackets and ‘??’ to define optional content.
|
||||
Use double parenthesis and ‘??’ to define optional content.
|
||||
</p>
|
||||
<p class="bottom-gutter-1-3">
|
||||
For example if you only want to show something to people who are under
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
</h3>
|
||||
<div id="personalization" class="usa-accordion__content usa-prose">
|
||||
<p class="bottom-gutter-1-3">
|
||||
Use double brackets to personalize your message:
|
||||
Use double parenthesis to personalize your message:
|
||||
</p>
|
||||
{{ usaInsetText({
|
||||
"text": "Hello ((first name)), your reference is ((ref number))",
|
||||
"classes": ""})
|
||||
}}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Send a document by email
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Use double brackets to add a placeholder field to your template. This will contain a secure link to download the document.
|
||||
Use double parenthesis to add a placeholder field to your template. This will contain a secure link to download the document.
|
||||
</p>
|
||||
{{ usaInsetText({
|
||||
"text": "Download your document at: ((link_to_file))",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p><a href="/join-notify">See if Notify is right for you</a></p>
|
||||
<p>Notify.gov is a product of the <a href="/studio">Public Benefits Studio</a>, a product accelerator inside
|
||||
the federal government. </p>
|
||||
</section>
|
||||
|
||||
@@ -40,8 +40,7 @@
|
||||
</p>
|
||||
<p>
|
||||
Texting not only helps programs reach people using a nearly-universal communication method, it is a cost effective
|
||||
way to do so. With Notify.gov <a href="/join-notify">you can get started for free</a>, allowing you to try out
|
||||
texting to complement your existing communications and outreach strategies.
|
||||
way to do so.
|
||||
</p>
|
||||
<h2 id="what-texting-is-best-for">What texting is best for</h2>
|
||||
<p>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{{ page_header('About your service') }}
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
|
||||
{{ form.name(param_extensions={"hint": {"text": "You can change this later"}}) }}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ Error
|
||||
<div class="usa-alert usa-alert--error" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<h1 class="usa-alert__heading banner-title" data-module="track-error" data-error-type="Missing placeholder columns"
|
||||
data-error-label="{{ upload_id }}">Your column names need to match the double brackets in your template</h1>
|
||||
data-error-label="{{ upload_id }}">Your column names need to match the double parenthesis in your template</h1>
|
||||
<p class="usa-alert__text">
|
||||
Your file is missing {{ recipients.missing_column_headers | formatted_list(
|
||||
conjunction='and',
|
||||
|
||||
@@ -101,6 +101,9 @@
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{{ usaButton({
|
||||
"text": button_text,
|
||||
"name": button_text
|
||||
}) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,19 +7,10 @@
|
||||
{% block content_column_content %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p>Is your organization interested in using Notify.gov? Find more information at <a href="/join-notify">Join
|
||||
Notify</a> or contact us at <a href="mailto:tts-notify@gsa.gov"
|
||||
<p>For any questions, contact us at <a href="mailto:tts-notify@gsa.gov"
|
||||
aria-label="contact us at tts-notify@gsa.gov">tts-notify@gsa.gov</a>.</p>
|
||||
<p>You can expect a response within one business day.</p>
|
||||
<div class="usa-summary-box maxw-tablet __web-inspector-hide-shortcut__" role="region"
|
||||
aria-label="For partnership inquiries">
|
||||
<div class="usa-summary-box__body">
|
||||
<div class="usa-summary-box__text">
|
||||
<p class="margin-0">If you are a current Notify.gov partner and have technical issues or questions, we are available at <a
|
||||
href="mailto:notify-support@gsa.gov"
|
||||
aria-label="Email Notify for technical questions at notify-support@gsa.gov">notify-support@gsa.gov</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>If you are a current Notify.gov partner and have technical issues or questions, we are available
|
||||
at <a href="mailto:notify-support@gsa.gov"
|
||||
aria-label="Email Notify for technical questions at notify-support@gsa.gov">notify-support@gsa.gov</a></p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": url_for("main.inbox", service_id=current_service.id) }) }}
|
||||
{{ usaBackLink({ "href": url_for("main.service_dashboard", service_id=current_service.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<div class="ajax-block">
|
||||
{% if current_service.inbound_sms_summary != None %}
|
||||
<a id="total-received" class="usa-link banner-dashboard" class="banner-dashboard" href="{{ url_for('.inbox', service_id=current_service.id) }}">
|
||||
<span class="banner-dashboard-count">
|
||||
{{ current_service.inbound_sms_summary.count|format_thousands }}
|
||||
</span>
|
||||
<span class="banner-dashboard-count-label">
|
||||
{{ current_service.inbound_sms_summary.count|message_count_label('sms', suffix='received') }}
|
||||
</span>
|
||||
{% if current_service.inbound_sms_summary.most_recent %}
|
||||
<span class="banner-dashboard-meta">
|
||||
latest message {{ current_service.inbound_sms_summary.most_recent | format_delta }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
{% from "components/table.html" import list_table, field, hidden_field_heading, right_aligned_field_heading, row_heading %}
|
||||
{% from "components/previous-next-navigation.html" import previous_next_navigation %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
{% if messages %}
|
||||
<p class="bottom-gutter-2-3 top-gutter-1-2">
|
||||
<a href="{{ url_for('.inbox_download', service_id=current_service.id) }}" download class="usa-link bold">Download these messages</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% call(item, row_number) list_table(
|
||||
messages,
|
||||
caption="Inbox",
|
||||
caption_visible=False,
|
||||
empty_message='When users text your service’s phone number ({}) you’ll see the messages here'.format(inbound_number),
|
||||
field_headings=[
|
||||
'From',
|
||||
'First two lines of message'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call field() %}
|
||||
<a
|
||||
class="usa-link file-list-filename"
|
||||
href="{{ url_for('.conversation', service_id=current_service.id, notification_id=item.id) }}#n{{ item.id }}"
|
||||
>
|
||||
{{ item.user_number | format_phone_number_human_readable }}
|
||||
</a>
|
||||
<span class="file-list-hint">{{ item.content }}</span>
|
||||
{% endcall %}
|
||||
{% call field(align='right') %}
|
||||
<span class="align-with-message-body">
|
||||
{{ item.created_at | format_delta }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
|
||||
{{ previous_next_navigation(prev_page, next_page) }}
|
||||
</div>
|
||||
@@ -1,25 +0,0 @@
|
||||
<div class="ajax-block-container">
|
||||
<div class="grid-row grid-gap">
|
||||
<div id="total-sms" class="grid-col-12 margin-top-2">
|
||||
<span class="big-number-with-status display-block margin-bottom-2">
|
||||
<p>
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if statistics['sms']['requested'] is number %}
|
||||
{{ "{:,}".format(statistics['sms']['requested']) }}
|
||||
{% else %}
|
||||
{{ statistics['sms']['requested'] }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ statistics['sms']['requested']|message_count_label('sms', suffix='sent') }} in the last seven days</span>
|
||||
</span>
|
||||
</p>
|
||||
<a class="usa-button usa-button--outline" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='sending,delivered,failed') }}">
|
||||
Details
|
||||
</a>
|
||||
{# Removing the failures area for now, as the user can click on the above link to see all the details.
|
||||
In the future state of the dashboard, the all statuses will be more apparent with data visualizations #}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,14 +46,16 @@
|
||||
</td>
|
||||
<td class="table-field template">{{ job.template_name }}</td>
|
||||
<td class="table-field time-sent">
|
||||
{% if job.scheduled_for and not job.processing_finished %}
|
||||
Scheduled for {{ job.scheduled_for|format_datetime_table }}
|
||||
{% elif job.processing_finished and not job.statistics|selectattr('status', 'equalto', 'sending')|list %}
|
||||
Sent on {{ job.processing_finished|format_datetime_table }}
|
||||
{% elif job.processing_started %}
|
||||
Sending since {{ job.processing_started|format_datetime_table }}
|
||||
{% 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 %}
|
||||
{% else %}
|
||||
Pending since {{ job.created_at|format_datetime_table }}
|
||||
Sent on {{ job.processing_started|format_datetime_table }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="table-field sender sender-column">{{ job.created_by.name }}</td>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
|
||||
|
||||
{% extends "withnav_template.html" %}
|
||||
|
||||
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading, hidden_field_heading, row_heading, notification_status_field, notification_carrier_field, notification_carrier_message_field %}
|
||||
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading,
|
||||
hidden_field_heading, row_heading, notification_status_field, notification_carrier_field,
|
||||
notification_carrier_message_field %}
|
||||
{% from "components/ajax-block.html" import ajax_block %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Dashboard
|
||||
Dashboard
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -15,29 +15,38 @@
|
||||
|
||||
<div class="dashboard margin-top-0 margin-bottom-2">
|
||||
|
||||
<h1 class="usa-sr-only">Dashboard</h1>
|
||||
{% if current_user.has_permissions('manage_templates') and not current_service.all_templates %}
|
||||
<h1 class="usa-sr-only">Dashboard</h1>
|
||||
{% if current_user.has_permissions('manage_templates') and not current_service.all_templates %}
|
||||
{% include 'views/dashboard/write-first-messages.html' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'upcoming') }}
|
||||
{% include 'views/dashboard/_upcoming.html' %}
|
||||
|
||||
<h2 class="font-body-2xl line-height-sans-2 margin-top-0">{{ current_service.name }} Dashboard</h2>
|
||||
<h2 class="font-body-2xl line-height-sans-2 margin-top-0">{{ current_service.name }} Dashboard</h2>
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent }}" data-messages-remaining="{{ messages_remaining }}">
|
||||
<div class="grid-row flex-align-center">
|
||||
<h2 id="chartTitle" class="margin-right-1">Total messages</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-tooltip usa-tooltip__information margin-right-0"
|
||||
data-position="top"
|
||||
title="Total messages track the sum of messages for the service: pending, failed, or delivered"
|
||||
>
|
||||
<span class="usa-sr-only">More information</span>
|
||||
i
|
||||
</button>
|
||||
</div>
|
||||
<svg id="totalMessageChart"></svg>
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
<div id="totalMessageTable" class="margin-0"></div>
|
||||
{% include 'views/dashboard/activity-table.html' %}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div id="totalMessageTable" class="margin-0"></div>
|
||||
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
||||
|
||||
{% include 'views/dashboard/activity-table.html' %}
|
||||
{% include 'views/dashboard/most-used-templates.html' %}
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/ajax-block.html" import ajax_block %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Received text messages
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": url_for('main.service_dashboard', service_id=current_service.id) }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Received text messages') }}
|
||||
|
||||
{{ ajax_block(
|
||||
partials,
|
||||
updates_url,
|
||||
'messages',
|
||||
) }}
|
||||
|
||||
{% endblock %}
|
||||
@@ -34,12 +34,13 @@
|
||||
<td><p>{{ item.template_folder }}</p></td>
|
||||
<td><p>{{ item.last_used|format_datetime_table}}</p></td>
|
||||
<td><p>{{ item.created_by }}</p></td>
|
||||
<td><p>{{ item.count }}</p></td>
|
||||
<td><p>{{ '{:,.0f}'.format(item.count) }}</p></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="/services/78409625-0c0a-485e-b82c-b19c8f4b1bdb/template-usage" class="usa-link show-more-no-border"><span>See templates by month</span></a>
|
||||
<a
|
||||
href="{{ url_for('.template_usage', service_id=current_service.id) }}" class="usa-link show-more-no-border"><span>See templates by month</span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -27,8 +27,8 @@
|
||||
{% set content_hint = 'Your service name will be added to the start of your message. You can turn this off in Settings.' %}
|
||||
{% endif %}
|
||||
|
||||
{% call form_wrapper() %}
|
||||
<div class="grid-row">
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
<div class="grid-container padding-0">
|
||||
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
|
||||
{{ form.name(param_extensions={
|
||||
"extra_form_group_classes": "margin-bottom-2",
|
||||
@@ -61,12 +61,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-row width-full">
|
||||
<div class="grid-row width-mobile-lg">
|
||||
<div class="tablet:grid-col-2 mobile-lg:grid-col-12">
|
||||
{{ page_footer('Save') }}
|
||||
</div>
|
||||
<div class="tablet:grid-col-10 mobile-lg:grid-col-12">
|
||||
<p class="usa-hint margin-top-5 tablet:margin-left-neg-2">
|
||||
<p class="usa-hint margin-top-5 tablet:margin-left-2">
|
||||
After saving, you'll have the option to send.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
<li>Add a placeholder using double brackets. For example: Hello ((first name)), your reference is ((ref number)).</li>
|
||||
<li>Add a placeholder using double parenthesis. For example: Hello ((first name)), your reference is ((ref number)).</li>
|
||||
<li>Select <b class="bold">Save</b>.</li>
|
||||
</ol>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
<li>Use double brackets and ?? to define optional content. For example, if you only want to show something to people who are under 18: ((under18??Please get your application signed by a parent or guardian.))</li>
|
||||
<li>Use double parenthesis and ?? to define optional content. For example, if you only want to show something to people who are under 18: ((under18??Please get your application signed by a parent or guardian.))</li>
|
||||
<li>Select <b class="bold">Save</b>.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ your recipient to manage their benefits and increase follow-through.</p>
|
||||
|
||||
<h3>To personalize your content</h3>
|
||||
<ol class="list">
|
||||
<li>Add a placeholder to your content by placing two brackets around the personalized elements.</li>
|
||||
<li>Add a placeholder to your content by placing two parenthesis around the personalized elements.</li>
|
||||
<li>You can manually enter the personalized content or you can upload a spreadsheet with the details and let Notify do the
|
||||
work for you.</li>
|
||||
</ol>
|
||||
@@ -78,7 +78,7 @@ all or part of the message contingent upon specific criteria associated with the
|
||||
|
||||
<h3>To add conditional content</h3>
|
||||
<ol class="list">
|
||||
<li>Use two brackets and ?? to define the conditional content.</li>
|
||||
<li>Use two parenthesis and ?? to define the conditional content.</li>
|
||||
<li>You can manually enter the conditional content or you can upload a spreadsheet with the personal details and let Notify
|
||||
do the work for you.</li>
|
||||
</ol>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
{% if not job.processing_finished %}
|
||||
{% if not job.finished_processing %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
{% endif %}
|
||||
{{ partials['status']|safe }}
|
||||
{% if not job.processing_finished %}
|
||||
{% if not job.finished_processing %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not finished %}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{% set product_highlights = [
|
||||
{
|
||||
"svg_src": "#chat",
|
||||
"card_heading": "Up to 100,000 messages to use over your first year*",
|
||||
"card_heading": "Up to 250,000 messages to use over your first year*",
|
||||
},
|
||||
{
|
||||
"svg_src": "#phone",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% elif error == 'message-too-long' %}
|
||||
{# the only row_errors we can get when sending one off messages is that the message is too long #}
|
||||
<div class="bottom-gutter">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% include "partials/check/message-too-long.html" %}
|
||||
@@ -77,13 +76,15 @@
|
||||
help='3' if help else 0
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<!-- <p>Placeholder: This message will be delivered to <b>400 phone numbers</b> and will use a total of <b>800 message parts</b>, leaving Washington DSHS with <b>249,200 message parts remaining</b>.</p> -->
|
||||
<h3>Does everything look good?</h3>
|
||||
{% if not error %}
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{{ usaButton({
|
||||
"text": button_text,
|
||||
"name": button_text
|
||||
}) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
28
app/templates/views/notify-service-ending.html
Normal file
28
app/templates/views/notify-service-ending.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set page_title = "Notify.gov Service Ending" %}
|
||||
|
||||
{% block per_page_title %}{{page_title}}{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p>GSA will no longer offer the Notify.gov service after June 8th, 2025. Current partners will continue to have service access until it is fully suspended. After May 30, partners will no longer be able to send messages, and the full service will be suspended by June 8, 2025.
|
||||
</p>
|
||||
<h2>Notify.gov Status Announcement</h2>
|
||||
<p class="text-italic">Notify.gov was in beta and will no longer be offered by GSA. Operations will be suspended on or before June 8. There is a chance that the service may need to stop sooner. GSA will keep partners informed to the extent possible.</p>
|
||||
|
||||
<p class="text-italic">All Notify.gov partners will be moved to trial mode no later than May 30, and will no longer be able to send messages. Partners will be able to access their templates and data through June 8.</p>
|
||||
|
||||
<p class="text-italic">We encourage government programs to explore ways to continue utilizing technology to meet people where they are. There are several commercial tools on the market that may work for Notify.gov use cases.</p>
|
||||
|
||||
<p class="text-italic">GSA is working to avoid potential disruptions to partners’ operations and ensure a smooth transition.</p>
|
||||
|
||||
<p class="text-italic">This decision is part of GSA’s renewed focus on its original mission: to be the backbone of the federal government’s administrative operations — streamlining processes, optimizing resources, and ensuring cost-effectiveness. GSA is driven to reducing federal spend, contributing to a more flexible and streamlined government, and delivering value for the American people.</p>
|
||||
|
||||
<p class="text-italic">We wish you all the best as you continue your work. </p>
|
||||
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -34,5 +34,8 @@
|
||||
<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 %}
|
||||
|
||||
@@ -34,9 +34,12 @@
|
||||
{% call form_wrapper(
|
||||
class='js-stick-at-top-when-scrolling send-one-off-form' if template.template_type != 'sms' else 'send-one-off-form',
|
||||
module="autofocus",
|
||||
data_kwargs={'force-focus': True}
|
||||
data_kwargs={'force-focus': True},
|
||||
data_force_focus=True
|
||||
) %}
|
||||
<div class="grid-row">
|
||||
{% set extra_class = "extra-tracking" if form.placeholder_value.label.text == "phone number" else "" %}
|
||||
{% set placeholder_id = "phone number" if form.placeholder_value.label.text == "phone number" else "" %}
|
||||
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
||||
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
|
||||
</div>
|
||||
|
||||
@@ -29,13 +29,6 @@
|
||||
</div>
|
||||
|
||||
<h2 class="font-body-lg">Your file needs to look like this example</h2>
|
||||
<p class="hint">
|
||||
Save your file as a
|
||||
<abbr title="Comma Separated Values">CSV</abbr><span aria-hidden="true">, </span>
|
||||
<abbr title="Tab Separated Values">TSV</abbr><span aria-hidden="true">, </span>
|
||||
<abbr title="Open Document Spreadsheet">ODS</abbr>
|
||||
or Microsoft Excel spreadsheet.
|
||||
</p>
|
||||
|
||||
<div class="spreadsheet" data-module="fullscreen-table">
|
||||
{% call(item, row_number) list_table(
|
||||
@@ -50,10 +43,14 @@
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
<p class="table-show-more-link">
|
||||
<a class="usa-link" href="{{ url_for('.get_example_csv', service_id=current_service.id, template_id=template.id) }}" download>Download this example (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
<p class="hint">
|
||||
Save your spreadsheet as a <abbr title="Comma Separated Values">CSV</abbr> file for bulk messaging. It is the most reliable when uploading your contact list. Start by downloading this example for your message template.
|
||||
</p>
|
||||
<p class="table-show-more-link">
|
||||
<a class="usa-link display-flex margin-top-1" href="{{ url_for('.get_example_csv', service_id=current_service.id, template_id=template.id) }}" download>Download this example (<abbr title="Comma separated values">CSV</abbr>)
|
||||
<img class="margin-left-05" src="{{ asset_url('img/material-icons/download.svg') }}" alt="" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 class="font-body-lg margin-bottom-1">Your file will populate this template:<br><span class="font-body-lg">({{ template.name }})</span></h2>
|
||||
{{ template|string }}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
label='Search by name',
|
||||
autofocus=True
|
||||
) }}
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% if has_organizations %}
|
||||
{{ form.organizations }}
|
||||
{{ sticky_page_footer('Save') }}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ page_header('Free text message allowance') }}
|
||||
{{ form.free_sms_allowance }}
|
||||
{{ page_footer('Save') }}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
See <a class="usa-link" href="{{ url_for(".pricing") }}">pricing</a> for the list
|
||||
of rates.
|
||||
</p>
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ form.enabled }}
|
||||
{{ page_footer('Save') }}
|
||||
{% endcall %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ page_header('Message batch limit') }}
|
||||
{{ form.message_limit }}
|
||||
{{ page_footer('Save') }}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ page_header('Rate limit') }}
|
||||
{{ form.rate_limit }}
|
||||
{{ page_footer('Save') }}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-10">
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ form.enabled(param_extensions={
|
||||
"fieldset": {
|
||||
"legend": {
|
||||
|
||||
@@ -20,12 +20,7 @@
|
||||
<p>
|
||||
You may send up to 250,000 text messages during the pilot period.
|
||||
</p>
|
||||
<!--<p>
|
||||
You have a free allowance of
|
||||
{{ '{:,}'.format(current_service.free_sms_fragment_limit) }} text messages each
|
||||
financial year.
|
||||
</p>-->
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ form.enabled }}
|
||||
{{ page_footer('Save') }}
|
||||
{% endcall %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{{ page_header('Start text messages with service name') }}
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ form.enabled }}
|
||||
{{ page_footer('Save') }}
|
||||
{% endcall %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{{ page_header('Add text message sender') }}
|
||||
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{{ form.sms_sender(param_extensions={
|
||||
"hint": {"text": "Up to 11 characters, letters, numbers and spaces only"}
|
||||
}) }}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Change text message sender') }}
|
||||
{% call form_wrapper() %}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% if inbound_number %}
|
||||
<p>
|
||||
<span class="bottom-gutter-1-3"> {{ sms_sender.sms_sender }} </span>
|
||||
|
||||
@@ -9,65 +9,70 @@ import usaButton %} {% block meta %}
|
||||
|
||||
<main id="main-content" role="main">
|
||||
{% block content %}
|
||||
<section class="usa-section--dark usa-hero padding-y-2" aria-label="Introduction">
|
||||
<section class="usa-section--dark usa-hero" aria-label="Introduction">
|
||||
<div class="grid-container padding-y-4">
|
||||
<div class="grid-row grid-gap display-flex flex-align-center">
|
||||
<div class="desktop:grid-col-8 tablet:grid-col-12">
|
||||
<h1 class="usa-hero__heading">
|
||||
<h1 class="font-sans-2xl usa-hero__heading">
|
||||
Reach people where they are with government-powered text messages
|
||||
</h1>
|
||||
<p class="desktop:font-sans-lg font-sans-md">
|
||||
<p class="font-sans-lg">
|
||||
Notify.gov is a text messaging service that helps federal, state,
|
||||
local, tribal and territorial governments more effectively
|
||||
communicate with the people they serve.
|
||||
</p>
|
||||
<div class="usa-button-group margin-bottom-5 flex-align-center">
|
||||
<a
|
||||
class="usa-button usa-button login-button login-button--primary margin-right-2"
|
||||
href="{{ initial_signin_url }}"
|
||||
>Sign in with
|
||||
<img
|
||||
src="{{ asset_url('images/logo-login.svg') }}"
|
||||
alt="Login.gov logo"
|
||||
/>
|
||||
</a>
|
||||
if you are an existing partner
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="desktop:grid-col-4 desktop:display-block display-none"
|
||||
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<img
|
||||
src="{{ asset_url('images/product/hero-icons.svg') }}"
|
||||
alt=""
|
||||
src="{{ asset_url('images/product/phone-text.png') }}"
|
||||
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-4">
|
||||
<div class="grid-row flex-align-center flex-justify-center">
|
||||
<div class="tablet:grid-col-4 mobile-lg:grid-col-12 display-flex flex-justify-center margin-bottom-6">
|
||||
<img
|
||||
src="{{ asset_url('images/product/couple-looking-at-phone.png') }}"
|
||||
alt="A couple smiling and looking at a cell phone together"/>
|
||||
</div>
|
||||
<div class="tablet:grid-col-6 tablet:margin-left-8 mobile-lg:grid-col-12">
|
||||
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
|
||||
Government texting made easy
|
||||
</h2>
|
||||
<p class="usa-body margin-bottom-5">
|
||||
Notify.gov is a text messaging platform built for government agencies.
|
||||
With minimal set-up and secure, personalized messaging, you can make
|
||||
one-way texting a part of your outreach program.
|
||||
</p>
|
||||
<a class="usa-button usa-button--big" href="/about">Learn more about Notify.gov</a>
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
|
||||
Government texting made easy
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Notify.gov is a text messaging platform built for government agencies.
|
||||
With minimal set-up and secure, personalized messaging, you can make
|
||||
one-way texting a part of your outreach program.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="usa-section usa-section__home usa-prose key-features padding-bottom-4">
|
||||
<h2 class="text-center margin-top-0">Key features</h2>
|
||||
<div class="grid-container home-cards margin-top-5">
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-sans-xl margin-top-0">Key features</h2>
|
||||
<div class="home-cards margin-top-5">
|
||||
<ul class="usa-card-group display-flex margin-bottom-4">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="img-container">
|
||||
<img
|
||||
src="{{ asset_url('images/internet.svg') }}"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="usa-card__header padding-bottom-0">
|
||||
<h3>Web-based</h3>
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/internet.svg') }}"
|
||||
alt="Globe on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Web-based</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Nothing to download or install</p>
|
||||
@@ -76,14 +81,13 @@ import usaButton %} {% block meta %}
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="img-container">
|
||||
<img
|
||||
src="{{ asset_url('images/fast.svg') }}"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="usa-card__header padding-bottom-0">
|
||||
<h3>Fast and easy</h3>
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/fast.svg') }}"
|
||||
alt="Stopwatch with a notification speech bubble with a star inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Fast and easy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>No technical expertise required</p>
|
||||
@@ -92,14 +96,13 @@ import usaButton %} {% block meta %}
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="img-container">
|
||||
<img
|
||||
src="{{ asset_url('images/status.svg') }}"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="usa-card__header padding-bottom-0">
|
||||
<h3>Track message delivery</h3>
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/status.svg') }}"
|
||||
alt="3 status messages, 2 successes and one failure"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Track message delivery</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>See which messages were received</p>
|
||||
@@ -108,16 +111,17 @@ import usaButton %} {% block meta %}
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="usa-card-group">
|
||||
<li class="usa-card tablet:grid-offset-2 tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="img-container">
|
||||
<img
|
||||
src="{{ asset_url('images/translation.svg') }}"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="usa-card__header padding-bottom-0">
|
||||
<h3>Send in recipients' preferred language</h3>
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/translation.svg') }}"
|
||||
alt="Speech bubbles with the letter A and the Chinese character for language"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send in recipients' preferred language
|
||||
</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Notify.gov has support for more than 30 character sets</p>
|
||||
@@ -126,14 +130,31 @@ import usaButton %} {% block meta %}
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="img-container">
|
||||
<img
|
||||
src="{{ asset_url('images/send.svg') }}"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/security.svg') }}"
|
||||
alt="Lock with code icon inside on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="padding-bottom-0">
|
||||
<h3 class="font-heading-md">Security and privacy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Limited data retention, encryption, and multi-factor
|
||||
authentication protect user data and manage risk with <br><a href="/about/security">our security efforts</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/send.svg') }}"
|
||||
alt="Paper airplane and a notification icon with the number 1 inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send bulk, customized, one-way messages
|
||||
</h3>
|
||||
</div>
|
||||
@@ -149,52 +170,33 @@ import usaButton %} {% block meta %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="usa-section usa-section__home usa-prose security-and-privacy padding-y-6 bg-base-lighter">
|
||||
<div class="grid-container">
|
||||
<div class="grid-row flex-align-center flex-justify-center">
|
||||
<div class="tablet:grid-col-7 mobile-lg:grid-col-12 margin-bottom-4">
|
||||
<h2 class="font-heading-xl margin-top-0 margin-bottom-3">
|
||||
Security and privacy
|
||||
</h2>
|
||||
<p class="usa-body margin-bottom-5">
|
||||
Limited data retention, encryption, and multi-factor authentication protect user data and manage risk.
|
||||
</p>
|
||||
<a class="usa-button usa-button--big" href="/join-notify">Our security efforts</a>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose grid-container padding-bottom-10">
|
||||
<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 />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</a>
|
||||
</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 %}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<ul class="list list-bullet">
|
||||
<li>For information on personalization, see <a href={{ url_for("main.how_to") }}>How to</a>.</li>
|
||||
<li>For help interpreting delivery reports, see <a href={{ url_for("main.message_status") }}>Delivery status</a>.</li>
|
||||
<li>For details on pricing and what counts as a message part, see <a href={{ url_for("main.pricing") }}>Pricing</a>.</li>
|
||||
</ul>
|
||||
|
||||
<p>If you have other questions, we are available at <a class="usa-link" href="mailto:notify-support@gsa.gov">notify-support@gsa.gov</a>.</p>
|
||||
|
||||
@@ -12,16 +12,9 @@
|
||||
<h2 class="font-body-lg">Limits while in trial mode</h2>
|
||||
<p>While your service is in trial mode you can only:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>send 50 text message parts per day</li>
|
||||
<li>send 50 text messages per day</li>
|
||||
<li>send messages to yourself and other people in your team</li>
|
||||
</ul>
|
||||
<p>Each text message is made up of one or more parts.</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>Generally, 160 characters is one part (and one text message).</li>
|
||||
<li>A text message of 160-306 characters is two parts.</li>
|
||||
</ul>
|
||||
<p>For more information on how message parts are calculated, see
|
||||
<a href="/using-notify/pricing">Tracking usage</a>.</p>
|
||||
|
||||
<h2 class="font-body-lg">Before going Live</h2>
|
||||
<p>Before you request to make your service live so you can send messages to clients:</p>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user