Compare commits
444 Commits
3-27-2025
...
python_man
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a64c65c49 | ||
|
|
31c58e0be7 | ||
|
|
86f8d66573 | ||
|
|
7b6a6ffe53 | ||
|
|
396459be67 | ||
|
|
a2fab31653 | ||
|
|
1e95628c56 | ||
|
|
3672b16315 | ||
|
|
e5dc5f12aa | ||
|
|
12b8dd6c4f | ||
|
|
36c7781e37 | ||
|
|
19b0e24602 | ||
|
|
71b865cc12 | ||
|
|
526402a7f4 | ||
|
|
a5399ae693 | ||
|
|
a9ff570a53 | ||
|
|
9c55f5035c | ||
|
|
3c63a345c9 | ||
|
|
c5613954a0 | ||
|
|
87489c8e55 | ||
|
|
8c1c9e877d | ||
|
|
8232d4ca21 | ||
|
|
229f8467c8 | ||
|
|
47571bc342 | ||
|
|
9f6e449b5e | ||
|
|
7942e02747 | ||
|
|
f9b96e9f83 | ||
|
|
b3736b03b1 | ||
|
|
00832b9555 | ||
|
|
2926da155e | ||
|
|
9430bbd97e | ||
|
|
8b3cb14666 | ||
|
|
0d8f1e9c01 | ||
|
|
56980e04ce | ||
|
|
b990363436 | ||
|
|
1981c02207 | ||
|
|
09cf2098af | ||
|
|
4cd727ab51 | ||
|
|
15501fb6ce | ||
|
|
595ae759d1 | ||
|
|
91b83ab749 | ||
|
|
9042f58e2c | ||
|
|
cf6c402e66 | ||
|
|
09f1c712bb | ||
|
|
f2b53628df | ||
|
|
f1b31eb85e | ||
|
|
33ab578d1f | ||
|
|
8ca029ea4d | ||
|
|
9d737e35d7 | ||
|
|
702c8c369e | ||
|
|
b81c6b20ec | ||
|
|
7dfe9c5988 | ||
|
|
4cbcf3fe27 | ||
|
|
69c8bbc6b7 | ||
|
|
6a606a467e | ||
|
|
3d44e76383 | ||
|
|
038f4e294a | ||
|
|
9aa7138902 | ||
|
|
86cd481786 | ||
|
|
b3e09219a8 | ||
|
|
31293557ef | ||
|
|
3fe74bea54 | ||
|
|
6aa84305f6 | ||
|
|
06f0eaa3c7 | ||
|
|
5c927d009b | ||
|
|
bbd19bb513 | ||
|
|
8b2139727e | ||
|
|
e95740b52b | ||
|
|
bb90f81571 | ||
|
|
45ce6d8e8e | ||
|
|
f824549e18 | ||
|
|
dabba6b696 | ||
|
|
ba0daeaf76 | ||
|
|
9bd81fa314 | ||
|
|
f5690c3384 | ||
|
|
e950e87ded | ||
|
|
32eb6c1e8f | ||
|
|
39b9cc08a3 | ||
|
|
2d92038f53 | ||
|
|
8aa074114e | ||
|
|
e1e9ba691b | ||
|
|
665d7b543b | ||
|
|
7ba4f3658e | ||
|
|
0afc93958c | ||
|
|
d27bc213d4 | ||
|
|
589c01ed75 | ||
|
|
29db4dcf4d | ||
|
|
0ad1b09763 | ||
|
|
e8e971ed87 | ||
|
|
30785f022c | ||
|
|
776dabc750 | ||
|
|
8be4538dbe | ||
|
|
0ea2043796 | ||
|
|
0f0a593998 | ||
|
|
3924029211 | ||
|
|
69cd75b7c0 | ||
|
|
1c048e140c | ||
|
|
abf7f1b4c7 | ||
|
|
ef6eeb6e2e | ||
|
|
134dde7899 | ||
|
|
9f647e5e81 | ||
|
|
99b1ad6af3 | ||
|
|
044dc90f6e | ||
|
|
3286e87096 | ||
|
|
40ece121b1 | ||
|
|
94c465f78c | ||
|
|
067a5efdc1 | ||
|
|
db885d14ec | ||
|
|
757803312d | ||
|
|
0ec9805bcc | ||
|
|
316e8d8807 | ||
|
|
790fe2d6f9 | ||
|
|
c2c57e72d5 | ||
|
|
444800fbda | ||
|
|
cb2ad27d1c | ||
|
|
1cbcc0b150 | ||
|
|
cc6d820277 | ||
|
|
206247f543 | ||
|
|
6cc152c07c | ||
|
|
45b4cdac56 | ||
|
|
0f9836aa8b | ||
|
|
1634201150 | ||
|
|
8d880bdfec | ||
|
|
98d1519844 | ||
|
|
c00e8c9ecd | ||
|
|
0713f69ec9 | ||
|
|
26677f116f | ||
|
|
5dcf967941 | ||
|
|
e658ed2584 | ||
|
|
1b6894139d | ||
|
|
2459ecd715 | ||
|
|
8079a95c7a | ||
|
|
802bda5169 | ||
|
|
3b8b38d6ad | ||
|
|
bc2738a97a | ||
|
|
0a42b1202d | ||
|
|
a0b4a2124a | ||
|
|
3c1574d070 | ||
|
|
a7787cb919 | ||
|
|
2a4d8abc26 | ||
|
|
17c12481c9 | ||
|
|
3788fe7a91 | ||
|
|
39682c4c50 | ||
|
|
bca36e1d9b | ||
|
|
20fcf8fa3b | ||
|
|
c8904e7d70 | ||
|
|
0e724eda99 | ||
|
|
6fc16db52b | ||
|
|
23cb68e3bc | ||
|
|
d43bf48686 | ||
|
|
9d57ac0294 | ||
|
|
f8c47f0194 | ||
|
|
28f9c3d9ff | ||
|
|
e8c0f985c7 | ||
|
|
8aa7a63f55 | ||
|
|
3f944e2447 | ||
|
|
bd884012d4 | ||
|
|
ecbde5ab1f | ||
|
|
33cfbe13dd | ||
|
|
81db86b463 | ||
|
|
c5d8792161 | ||
|
|
3d07e6f898 | ||
|
|
1a1f9a2ab4 | ||
|
|
4728ee20e8 | ||
|
|
9e63f9b096 | ||
|
|
ecfde62648 | ||
|
|
be0dcd103f | ||
|
|
17544c4b98 | ||
|
|
83515cd321 | ||
|
|
d1d3a0d426 | ||
|
|
8b6edf93db | ||
|
|
f64a9a0072 | ||
|
|
0ff42e4561 | ||
|
|
80314ad76d | ||
|
|
b017a3c1e6 | ||
|
|
6193125492 | ||
|
|
0b6e52349d | ||
|
|
96cbebefed | ||
|
|
9b8d0c2b2f | ||
|
|
abb3ba64d9 | ||
|
|
be038daa0a | ||
|
|
abf822cf39 | ||
|
|
63d96d46b8 | ||
|
|
5cd703eb2f | ||
|
|
2c08d638a0 | ||
|
|
b9e08a5bc8 | ||
|
|
540ee74243 | ||
|
|
ac69799108 | ||
|
|
15dcf7474f | ||
|
|
39972c341a | ||
|
|
5887c1cf5c | ||
|
|
35176328ca | ||
|
|
e64446f5be | ||
|
|
a9901fc951 | ||
|
|
eb3780e5b6 | ||
|
|
d5131c61f9 | ||
|
|
b9c50c1e5d | ||
|
|
000b2b7819 | ||
|
|
34ed31d611 | ||
|
|
c844a6bed8 | ||
|
|
566228d051 | ||
|
|
f79bb3d95a | ||
|
|
c87a5d6bc6 | ||
|
|
7fef3fff26 | ||
|
|
eff92773cc | ||
|
|
f771f2374d | ||
|
|
0e48ed2716 | ||
|
|
149d8e3ce5 | ||
|
|
e5dce090e6 | ||
|
|
4b138711e2 | ||
|
|
824364f2b6 | ||
|
|
50bdf916ba | ||
|
|
285bee3809 | ||
|
|
8e28cdbb56 | ||
|
|
02023aa775 | ||
|
|
2adb87dfe4 | ||
|
|
43548a823e | ||
|
|
db4c02532a | ||
|
|
598e5eca91 | ||
|
|
b48377e4b4 | ||
|
|
ccdfd0c10e | ||
|
|
0917ddaa23 | ||
|
|
7fbed553aa | ||
|
|
0710372db4 | ||
|
|
db7d59c8c8 | ||
|
|
c5228d9557 | ||
|
|
eb2d4798bd | ||
|
|
3ed99ccd45 | ||
|
|
b91e687d17 | ||
|
|
5eb08b8042 | ||
|
|
aa59ac1d9c | ||
|
|
c825878c45 | ||
|
|
e6b76486f4 | ||
|
|
69ae1ea0e1 | ||
|
|
b468620ead | ||
|
|
4279467b16 | ||
|
|
f0ff50eb63 | ||
|
|
86b4547499 | ||
|
|
a6145bec19 | ||
|
|
1c795253d4 | ||
|
|
6c16482577 | ||
|
|
bf03391b6d | ||
|
|
e04806b1eb | ||
|
|
7410a00519 | ||
|
|
fb20ea6e1c | ||
|
|
cd40580231 | ||
|
|
b78a430ef9 | ||
|
|
7a52290bb7 | ||
|
|
4b5fee0bba | ||
|
|
350526f4ad | ||
|
|
fb5f7d969e | ||
|
|
2d6afb7904 | ||
|
|
670d82b72c | ||
|
|
ec7af325df | ||
|
|
23782a83d0 | ||
|
|
609c472cff | ||
|
|
386b3f8348 | ||
|
|
8bf58cdca9 | ||
|
|
6b92941156 | ||
|
|
0ccbe17b3b | ||
|
|
ae305227bb | ||
|
|
7130925b9a | ||
|
|
f484a9c432 | ||
|
|
6f6402e36a | ||
|
|
714e5011c3 | ||
|
|
6bfa9ede76 | ||
|
|
57023b3a3a | ||
|
|
0e31d34672 | ||
|
|
1b9e19072e | ||
|
|
55dc8e31ca | ||
|
|
f9a08dffe0 | ||
|
|
f661ea39ca | ||
|
|
431dbb10dd | ||
|
|
25524324c5 | ||
|
|
5abc0a4c8e | ||
|
|
3328853bd0 | ||
|
|
d6baaebef8 | ||
|
|
b6a6ffb2ab | ||
|
|
b01a62225e | ||
|
|
522340f52b | ||
|
|
fe42f5ef64 | ||
|
|
b4afcb07aa | ||
|
|
5d3364a114 | ||
|
|
70b149476c | ||
|
|
4a1ac9b85c | ||
|
|
10c61440c9 | ||
|
|
34fdf7a1ca | ||
|
|
20cbb0a8db | ||
|
|
14024a28ca | ||
|
|
280e348fa6 | ||
|
|
72f36a4d15 | ||
|
|
0b866aa7a7 | ||
|
|
77e9475ed8 | ||
|
|
b4e239c9bb | ||
|
|
c016530a7d | ||
|
|
2779e398f6 | ||
|
|
8dfa881f26 | ||
|
|
5f258cb699 | ||
|
|
a53df8998c | ||
|
|
e3843f33c6 | ||
|
|
20e0dc6c3e | ||
|
|
e9029eef09 | ||
|
|
d0d6113a05 | ||
|
|
232c6cb9e1 | ||
|
|
ed238bfe6f | ||
|
|
a3eeeb2b2c | ||
|
|
54d8996e2a | ||
|
|
88e9f1dec6 | ||
|
|
7de1a9b944 | ||
|
|
b4a8eb7210 | ||
|
|
f975953aa7 | ||
|
|
34b39acf38 | ||
|
|
3e0e290dc0 | ||
|
|
8c32af4ee1 | ||
|
|
5464da155a | ||
|
|
01678ce67a | ||
|
|
67b7dc28b4 | ||
|
|
d4a2584dfb | ||
|
|
638a4f20da | ||
|
|
fbd70b7c38 | ||
|
|
06ce617f0a | ||
|
|
1afe8121b8 | ||
|
|
9d7b6d3147 | ||
|
|
7b2d031591 | ||
|
|
ae3be157e6 | ||
|
|
16b1429eb0 | ||
|
|
64ac382af9 | ||
|
|
ca1e87a54b | ||
|
|
da695557b0 | ||
|
|
3e18c8f595 | ||
|
|
d3e17c743b | ||
|
|
6d398f7301 | ||
|
|
e96f01f4a9 | ||
|
|
9eec7cdba5 | ||
|
|
e59ddfcae5 | ||
|
|
38d8bdea0a | ||
|
|
f8eb512aab | ||
|
|
6dd44fdc2d | ||
|
|
1967195da0 | ||
|
|
fb11fbfba3 | ||
|
|
17a7080f46 | ||
|
|
a962da9c7d | ||
|
|
3a468c77aa | ||
|
|
d8ca53e08b | ||
|
|
bea926f103 | ||
|
|
6f8f7346ca | ||
|
|
8dbcc59726 | ||
|
|
f3c3a60875 | ||
|
|
253934a495 | ||
|
|
fff81870e5 | ||
|
|
970483ef0b | ||
|
|
ba661a0c53 | ||
|
|
f5ce055079 | ||
|
|
29c2eafbd7 | ||
|
|
6ecbc7ebab | ||
|
|
f3688d62d5 | ||
|
|
1f13176fa6 | ||
|
|
09c6d0445d | ||
|
|
c328353e18 | ||
|
|
e9e0601237 | ||
|
|
88f29a6727 | ||
|
|
9d4d55a670 | ||
|
|
3211c5a16f | ||
|
|
9c8aa9c0e2 | ||
|
|
c2508f7230 | ||
|
|
b5675e586e | ||
|
|
823b490331 | ||
|
|
e69d6f807f | ||
|
|
ee8213f670 | ||
|
|
c43174be78 | ||
|
|
548072fe66 | ||
|
|
7f3e368427 | ||
|
|
caed2ad5d6 | ||
|
|
3401b994cd | ||
|
|
d5798bb818 | ||
|
|
0cf9abf5e8 | ||
|
|
0bdf0cb073 | ||
|
|
b8b7c2df69 | ||
|
|
2ca7f5d2b0 | ||
|
|
db37a4de94 | ||
|
|
b3b31d0732 | ||
|
|
ef779daec1 | ||
|
|
50df9c79a2 | ||
|
|
e4a4217dba | ||
|
|
3179471122 | ||
|
|
f8c7f6b817 | ||
|
|
193184983d | ||
|
|
38451d77ad | ||
|
|
032b9927aa | ||
|
|
82934daa02 | ||
|
|
1c2c9fd57a | ||
|
|
417356bc28 | ||
|
|
f379e828de | ||
|
|
7a29252081 | ||
|
|
06c238269f | ||
|
|
91f3e6a557 | ||
|
|
2064fc864a | ||
|
|
273232df40 | ||
|
|
c1e1e34a17 | ||
|
|
7e4947389c | ||
|
|
bf9537ccd4 | ||
|
|
4225470b1e | ||
|
|
c49d998f9e | ||
|
|
c9d9c7a3a6 | ||
|
|
35a2d940a0 | ||
|
|
e1c6d41f78 | ||
|
|
e419d843a6 | ||
|
|
c01a6a84f3 | ||
|
|
7cd8be22f6 | ||
|
|
f9dd331421 | ||
|
|
78b30e3d76 | ||
|
|
bd4e1d751a | ||
|
|
f71b03a45c | ||
|
|
f1ccbe6f52 | ||
|
|
bbe1e44366 | ||
|
|
685deebae4 | ||
|
|
918016d4af | ||
|
|
f87f0a1c9e | ||
|
|
869f76c846 | ||
|
|
3946e7bb3a | ||
|
|
56ee7ec376 | ||
|
|
5dad9e8f0f | ||
|
|
23c5d7aff1 | ||
|
|
d2dd249b61 | ||
|
|
3e410ba5b2 | ||
|
|
f55d47e3d1 | ||
|
|
7813ce7914 | ||
|
|
a1802a3439 | ||
|
|
8897b30b81 | ||
|
|
5741c572f0 | ||
|
|
b0d8eda0f4 | ||
|
|
97b8f70c1f | ||
|
|
9e5475cd43 | ||
|
|
6eaa471a5c | ||
|
|
21980d2bdf | ||
|
|
96890199f3 | ||
|
|
f246cc7c51 | ||
|
|
d92e2a7c04 | ||
|
|
94b791c869 | ||
|
|
700c76807b | ||
|
|
e0e8c56a61 | ||
|
|
d79ece2d01 | ||
|
|
0108bcf91f | ||
|
|
606b124912 |
64
.ds.baseline
@@ -127,6 +127,16 @@
|
||||
}
|
||||
],
|
||||
"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",
|
||||
@@ -151,7 +161,7 @@
|
||||
"filename": "app/config.py",
|
||||
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
|
||||
"is_verified": false,
|
||||
"line_number": 118,
|
||||
"line_number": 123,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -389,26 +399,6 @@
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/templates/new/components/head.html": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/templates/new/components/head.html",
|
||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||
"is_verified": false,
|
||||
"line_number": 33,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/templates/old/admin_template.html": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/templates/old/admin_template.html",
|
||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||
"is_verified": false,
|
||||
"line_number": 18,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"deploy-config/sandbox.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
@@ -642,37 +632,7 @@
|
||||
"line_number": 3266,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py",
|
||||
"hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/encryption/test_encryption_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py",
|
||||
"hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605",
|
||||
"is_verified": false,
|
||||
"line_number": 13,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py",
|
||||
"hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-03-20T18:22:36Z"
|
||||
"generated_at": "2025-06-04T16:12:20Z"
|
||||
}
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
@@ -9,10 +9,9 @@ body:
|
||||
label: Log In
|
||||
options:
|
||||
- label: notify-staging.app.cloud.gov resolves
|
||||
- label: Clicking sign in takes you to login page
|
||||
- label: 2FA code is sent promptly
|
||||
- label: 2FA code input screen appears as expected
|
||||
- label: After code is entered, you are brought to the “choose service” screen
|
||||
- label: Clicking sign in takes you to Login.gov
|
||||
- label: Can sign in via Login.gov
|
||||
- label: After successful login, you are brought to the “choose service” screen
|
||||
- label: Choosing a service brings you to the dashboard for that service
|
||||
|
||||
- type: checkboxes
|
||||
@@ -72,11 +71,10 @@ body:
|
||||
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that person’s profile
|
||||
- label: Clicking on “Remove this team member” prompts a confirmation
|
||||
- label: Clicking the confirmation removes the team member
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: sign_out
|
||||
attributes:
|
||||
label: Sign Out
|
||||
options:
|
||||
- label: Hitting “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
- label: Clicking “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
12
.github/actions/setup-project/action.yml
vendored
@@ -9,13 +9,19 @@ runs:
|
||||
sudo apt-get update \
|
||||
&& sudo apt-get install -y --no-install-recommends \
|
||||
libcurl4-openssl-dev
|
||||
- name: Set up Python 3.12.3
|
||||
- name: Set up Python 3.12.2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.3"
|
||||
python-version: "3.12.2"
|
||||
- name: Install poetry
|
||||
shell: bash
|
||||
run: pip install poetry==1.8.5
|
||||
run: pip install poetry==2.1.3
|
||||
- name: Install poetry export plugin
|
||||
shell: bash
|
||||
run: poetry self add poetry-plugin-export
|
||||
- name: Downgrade virtualenv to compatible version
|
||||
shell: bash
|
||||
run: pip install "virtualenv<20.30"
|
||||
- name: Install application dependencies
|
||||
shell: bash
|
||||
run: make bootstrap
|
||||
|
||||
4
.github/dependabot.yml
vendored
@@ -10,9 +10,9 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "A-Shumway42"
|
||||
- "alexjanousekGSA"
|
||||
reviewers:
|
||||
- "A-Shumway42"
|
||||
- "alexjanousekGSA"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
133
.github/workflows/checks.yml
vendored
@@ -47,78 +47,77 @@ jobs:
|
||||
- name: Run js tests
|
||||
run: npm test
|
||||
- name: Run py tests with coverage
|
||||
run: poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
run: poetry run coverage run --omit=*/notifications_utils/* -m pytest -vv --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
- name: Check coverage threshold
|
||||
run: poetry run coverage report --fail-under=90
|
||||
|
||||
# TODO FIX THIS!
|
||||
# end-to-end-tests:
|
||||
# if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
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
|
||||
@@ -140,13 +139,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
PYSEC-2024-60
|
||||
PYSEC-2022-43162
|
||||
PYSEC-2023-312
|
||||
- name: Run npm audit
|
||||
run: make npm-audit
|
||||
|
||||
@@ -167,6 +165,7 @@ jobs:
|
||||
run: make run-flask &
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: scanning
|
||||
FEATURE_SOCKET_ENABLED: true
|
||||
- name: Run OWASP Baseline Scan
|
||||
uses: zaproxy/action-baseline@v0.14.0
|
||||
with:
|
||||
|
||||
4
.github/workflows/daily_checks.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
PYSEC-2023-312
|
||||
- name: Run npm audit
|
||||
run: make npm-audit
|
||||
|
||||
|
||||
6
.github/workflows/deploy-demo.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
@@ -63,6 +63,8 @@ jobs:
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-demo.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
|
||||
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -87,6 +89,8 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var API_PUBLIC_URL="$API_PUBLIC_URL"
|
||||
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
|
||||
--strategy rolling
|
||||
|
||||
- name: Deploy egress proxy
|
||||
|
||||
6
.github/workflows/deploy-prod.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
@@ -63,6 +63,8 @@ jobs:
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://beta.notify.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
|
||||
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -87,6 +89,8 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var API_PUBLIC_URL="$API_PUBLIC_URL"
|
||||
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
|
||||
--strategy rolling
|
||||
|
||||
- name: Deploy egress proxy
|
||||
|
||||
6
.github/workflows/deploy.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
@@ -69,6 +69,8 @@ jobs:
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
|
||||
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -93,6 +95,8 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var API_PUBLIC_URL="$API_PUBLIC_URL"
|
||||
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
|
||||
--strategy rolling
|
||||
|
||||
|
||||
|
||||
5
.gitignore
vendored
@@ -14,6 +14,11 @@
|
||||
*.XLSX
|
||||
|
||||
## Non user files allowed to be commited
|
||||
!app/assets/pdf/wa_case_study.pdf
|
||||
!app/assets/pdf/best-practices-for-texting-the-public.pdf
|
||||
!app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf
|
||||
!app/assets/pdf/best-practices-section-outline.pdf
|
||||
!app/assets/pdf/standing-up-your-own-notify.pdf
|
||||
!app/assets/pdf/tcpa_overview.pdf
|
||||
!app/assets/pdf/investing-notifications-tts-public-benefits-memo.pdf
|
||||
!app/assets/pdf/out-of-pilot-announcement.pdf
|
||||
|
||||
@@ -8,6 +8,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
exclude: ^app/assets/pdf/.*\.pdf$
|
||||
- id: debug-statements
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
|
||||
2
.profile
@@ -6,4 +6,4 @@
|
||||
export NEW_RELIC_PROXY_HOST=$egress_proxy
|
||||
export http_proxy=$egress_proxy
|
||||
export https_proxy=$egress_proxy
|
||||
export no_proxy="apps.internal"
|
||||
export no_proxy="apps.internal,s3-fips.us-gov-west-1.amazonaws.com"
|
||||
|
||||
30
Makefile
@@ -6,6 +6,7 @@ APP_VERSION_FILE = app/version.py
|
||||
|
||||
GIT_BRANCH ?= $(shell git symbolic-ref --short HEAD 2> /dev/null || echo "detached")
|
||||
GIT_COMMIT ?= $(shell git rev-parse HEAD 2> /dev/null || echo "")
|
||||
GIT_HOOKS_PATH ?= $(shell git config --global core.hooksPath || echo "")
|
||||
|
||||
VIRTUALENV_ROOT := $(shell [ -z $$VIRTUAL_ENV ] && echo $$(pwd)/venv || echo $$VIRTUAL_ENV)
|
||||
|
||||
@@ -14,16 +15,27 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
|
||||
## DEVELOPMENT
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: generate-version-file ## Set up everything to run the app
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
bootstrap: ## Set up everything to run the app
|
||||
make generate-version-file
|
||||
poetry sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
poetry run pre-commit install
|
||||
source $(NVMSH) --no-use && nvm install && npm install
|
||||
source $(NVMSH) && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: bootstrap-with-git-hooks
|
||||
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
|
||||
make generate-version-file
|
||||
poetry sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
git config --global --unset-all core.hooksPath
|
||||
poetry run pre-commit install
|
||||
git config --global core.hookspath "${GIT_HOOKS_PATH}"
|
||||
source $(NVMSH) --no-use && nvm install && npm install
|
||||
source $(NVMSH) && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: watch-frontend
|
||||
watch-frontend: ## Build frontend and watch for changes
|
||||
source $(NVMSH) && npm run watch
|
||||
@@ -53,7 +65,7 @@ generate-version-file: ## Generates the app version file
|
||||
@echo -e "__git_commit__ = \"${GIT_COMMIT}\"\n__time__ = \"${DATE}\"" > ${APP_VERSION_FILE}
|
||||
|
||||
.PHONY: test
|
||||
test: py-lint py-test js-lint js-test ## Run tests
|
||||
test: py-lint py-test js-test ## Run tests
|
||||
|
||||
.PHONY: py-lint
|
||||
py-lint: ## Run python linting scanners and black
|
||||
@@ -83,18 +95,18 @@ too-complex:
|
||||
py-test: export NEW_RELIC_ENVIRONMENT=test
|
||||
py-test: ## Run python unit tests
|
||||
poetry run coverage run -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
poetry run coverage report --fail-under=96
|
||||
poetry run coverage report --fail-under=93
|
||||
poetry run coverage html -d .coverage_cache
|
||||
|
||||
.PHONY: dead-code
|
||||
dead-code: ## 60% is our aspirational goal, but currently breaks the build
|
||||
poetry run vulture ./app ./notifications_utils --min-confidence=100
|
||||
|
||||
|
||||
.PHONY: e2e-test
|
||||
e2e-test: export NEW_RELIC_ENVIRONMENT=test
|
||||
e2e-test: ## Run end-to-end integration tests; note that --browser webkit isn't currently working
|
||||
DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
|
||||
@echo "Running E2E tests in path: $${TESTPATH:-tests/end_to_end}"
|
||||
@bash -c 'DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox "$${TESTPATH:-tests/end_to_end}"'
|
||||
|
||||
.PHONY: js-lint
|
||||
js-lint: ## Run javascript linting scanners
|
||||
@@ -115,7 +127,7 @@ py-lock: ## Syncs dependencies and updates lock file without performing recursiv
|
||||
|
||||
.PHONY: freeze-requirements
|
||||
freeze-requirements: ## create static requirements.txt
|
||||
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
poetry export --output requirements.txt
|
||||
|
||||
.PHONY: pip-audit
|
||||
pip-audit:
|
||||
|
||||
18
README.md
@@ -31,7 +31,7 @@ You will need the following items:
|
||||
[setting up an account](https://cloud.gov/sign-up/) (requires a `.mil`,
|
||||
`.gov`, or `.fed.us` email address) and getting access to the
|
||||
`notify-local-dev` and `notify-staging` spaces.
|
||||
- Admin priviliges and SSH access on your machine; you may need to work with
|
||||
- Admin privileges and SSH access on your machine; you may need to work with
|
||||
your organization's IT support staff if you're not sure or don't currently
|
||||
have this access.
|
||||
|
||||
@@ -308,6 +308,22 @@ you'll be set with an upgraded version of Python.
|
||||
|
||||
_If you're not sure about the details of your current virtual environment, you can run `poetry env info` to get more information. If you've been using `pyenv` for everything, you can also see all available virtual environments with `pyenv virtualenvs`._
|
||||
|
||||
|
||||
#### Poetry upgrades ####
|
||||
|
||||
If you are doing a new project setup, then after you install poetry you need to install the export plugin
|
||||
|
||||
```sh
|
||||
poetry self add poetry-plugin-export
|
||||
```
|
||||
|
||||
If you are upgrading from poetry 1.8.5, you need to do this:
|
||||
|
||||
```sh
|
||||
curl -sSL https://install.python-poetry.org | python3 - --version 2.1.3
|
||||
poetry self add poetry-export-plugin
|
||||
```
|
||||
|
||||
#### Updating the .env file for Login.gov
|
||||
|
||||
To configure the application for Login.gov, you will need to update the following environment variables in the .env file:
|
||||
|
||||
@@ -109,6 +109,7 @@ from app.notify_client.template_statistics_api_client import template_statistics
|
||||
from app.notify_client.upload_api_client import upload_api_client
|
||||
from app.notify_client.user_api_client import user_api_client
|
||||
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
|
||||
from app.utils.api_health import is_api_down
|
||||
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
||||
from notifications_utils import logging, request_helper
|
||||
from notifications_utils.formatters import (
|
||||
@@ -140,11 +141,15 @@ navigation = {
|
||||
def _csp(config):
|
||||
asset_domain = config["ASSET_DOMAIN"]
|
||||
logo_domain = config["LOGO_CDN_DOMAIN"]
|
||||
api_public_url = config["API_PUBLIC_URL"]
|
||||
api_public_ws_url = config["API_PUBLIC_WS_URL"]
|
||||
|
||||
return {
|
||||
"default-src": ["'self'", asset_domain],
|
||||
"frame-src": [
|
||||
"https://www.youtube.com",
|
||||
"https://www.youtube-nocookie.com",
|
||||
"https://www.googletagmanager.com",
|
||||
],
|
||||
"frame-ancestors": "'none'",
|
||||
"form-action": "'self'",
|
||||
@@ -157,11 +162,14 @@ def _csp(config):
|
||||
"https://www.googletagmanager.com",
|
||||
"https://www.google-analytics.com",
|
||||
"https://dap.digitalgov.gov",
|
||||
"https://cdn.socket.io",
|
||||
],
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.google-analytics.com",
|
||||
f"{api_public_url}",
|
||||
f"{api_public_ws_url}",
|
||||
],
|
||||
"style-src": ["'self'", asset_domain],
|
||||
"img-src": ["'self'", asset_domain, logo_domain],
|
||||
@@ -169,15 +177,25 @@ def _csp(config):
|
||||
|
||||
|
||||
def create_app(application):
|
||||
# @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.after_request
|
||||
def add_csp_header(response):
|
||||
existing_csp = response.headers.get("Content-Security-Policy", "")
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
existing_csp + "; form-action 'self';"
|
||||
)
|
||||
return response
|
||||
|
||||
@application.context_processor
|
||||
def inject_feature_flags():
|
||||
# this is where feature flags can be easily added as a dictionary within context
|
||||
feature_socket_enabled = application.config.get("FEATURE_SOCKET_ENABLED", False)
|
||||
return dict(
|
||||
FEATURE_SOCKET_ENABLED=feature_socket_enabled,
|
||||
)
|
||||
|
||||
@application.context_processor
|
||||
def inject_is_api_down():
|
||||
return {"is_api_down": is_api_down()}
|
||||
|
||||
@application.context_processor
|
||||
def inject_initial_signin_url():
|
||||
|
||||
1
app/assets/images/api-error.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m468.547 144.82h-63.047v-121.32c0-12.958-10.542-23.5-23.5-23.5h-324c-12.958 0-23.5 10.542-23.5 23.5v84.833c.344 9.931 14.659 9.923 15 0v-33.333h341v69.82h-121.998c-14.312 0-25.954 11.574-25.954 25.8v83.174l-28.866 19.221c-5.511 3.444-5.51 12.168.001 15.611l28.865 19.22v83.174c0 14.226 11.643 25.799 25.954 25.799h121.998v71.68c0 4.687-3.813 8.5-8.5 8.5h-324c-4.687 0-8.5-3.813-8.5-8.5v-20h131.5c7.444 0 13.5-6.056 13.5-13.5v-127.999c0-7.444-6.056-13.5-13.5-13.5h-131.5v-168.021c-.344-9.931-14.659-9.923-15 0v168.021h-3.5c-7.444 0-13.5 6.056-13.5 13.5v128c0 7.444 6.056 13.5 13.5 13.5h3.5v20c0 12.958 10.542 23.5 23.5 23.5h324c12.958 0 23.5-10.542 23.5-23.5v-71.68h11.5c9.931-.344 9.923-14.659 0-15h-148.498c-6.04 0-10.954-4.845-10.954-10.799v-85.173c0-3.779-1.882-7.288-5.031-9.384l-23.492-15.643 23.492-15.644c3.15-2.098 5.031-5.606 5.031-9.384v-85.173c0-5.955 4.914-10.8 10.954-10.8h200.045c6.04 0 10.953 4.845 10.953 10.8v220.401c0 5.955-4.913 10.799-10.953 10.799h-16.436c-9.931.344-9.923 14.659 0 15h16.436c14.311 0 25.953-11.574 25.953-25.799v-220.401c0-14.226-11.643-25.8-25.953-25.8zm-256.047-129.82v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.333 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.334 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-52.333 45v-36.5c0-4.687 3.813-8.5 8.5-8.5h13.833v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.334v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.333v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h154.5c4.687 0 8.5 3.813 8.5 8.5v36.5zm-17 268.5h147v125h-147z"/><path d="m451.356 344.514c2.439-4.507 2.214-9.973-.589-14.264l-69.093-105.771c-5.242-8.361-18.107-8.359-23.35 0l-69.092 105.771c-2.803 4.291-3.028 9.756-.589 14.263s7.139 7.307 12.264 7.307h138.186c5.125 0 9.824-2.799 12.263-7.306zm-148.499-7.694 67.143-102.786 67.143 102.786z"/><path d="m362.5 266.32v31.445c.344 9.931 14.659 9.923 15 0v-31.445c-.344-9.931-14.659-9.923-15 0z"/><path d="m370 310.82c-4.143 0-7.5 3.358-7.5 7.5v2.5c.344 9.931 14.659 9.923 15 0v-2.5c0-4.142-3.357-7.5-7.5-7.5z"/><path d="m91.375 123.094h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 149.316h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 175.539h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m91.375 225.76h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 251.982h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 278.205h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m139.354 356.038c-2.93-2.929-7.678-2.929-10.607 0l-22.747 22.747-22.747-22.747c-2.93-2.929-7.678-2.929-10.607 0s-2.929 7.678 0 10.606l22.748 22.748-22.748 22.748c-2.929 2.929-2.929 7.678 0 10.606 2.93 2.929 7.677 2.929 10.607 0l22.747-22.747 22.747 22.747c2.93 2.929 7.677 2.929 10.607 0 2.929-2.929 2.929-7.678 0-10.606l-22.748-22.748 22.748-22.748c2.929-2.928 2.929-7.677 0-10.606z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -1,27 +0,0 @@
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
Modules.Autofocus = function() {
|
||||
this.start = function(component) {
|
||||
var $component = $(component),
|
||||
forceFocus = $component.data('forceFocus');
|
||||
|
||||
// if the page loads with a scroll position, we can't assume the item to focus onload
|
||||
// is still where users intend to start
|
||||
if (($(window).scrollTop() > 0) && !forceFocus) { return; }
|
||||
|
||||
// See if the component itself is something we want to send focus to
|
||||
var target = $component.filter('input, textarea, select');
|
||||
|
||||
// Otherwise look inside the component to see if there are any elements
|
||||
// we want to send focus to
|
||||
if (target.length === 0) {
|
||||
target = $('input, textarea, select', $component);
|
||||
}
|
||||
|
||||
target.eq(0).trigger('focus');
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
})(window.GOVUK.Modules);
|
||||
@@ -1,29 +0,0 @@
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
let isSixDigitHex = value => value.match(/^#[0-9A-F]{6}$/i);
|
||||
let colourOrWhite = value => isSixDigitHex(value) ? value : '#FFFFFF';
|
||||
|
||||
Modules.ColourPreview = function() {
|
||||
|
||||
this.start = component => {
|
||||
|
||||
this.$input = $(component);
|
||||
|
||||
this.$input.closest('.usa-form-group').append(
|
||||
this.$preview = $('<span class="textbox-colour-preview"></span>')
|
||||
);
|
||||
|
||||
this.$input
|
||||
.on('input', this.update)
|
||||
.trigger('input');
|
||||
|
||||
};
|
||||
|
||||
this.update = () => this.$preview.css(
|
||||
'background', colourOrWhite(this.$input.val())
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
})(window.GOVUK.Modules);
|
||||
@@ -1,34 +1,69 @@
|
||||
function announceUploadStatusFromElement() {
|
||||
const srRegion = document.getElementById('upload-status-live');
|
||||
const success = document.getElementById('upload-success');
|
||||
const error = document.getElementById('upload-error');
|
||||
|
||||
if (!srRegion) return;
|
||||
|
||||
const message = error?.textContent || success?.textContent;
|
||||
|
||||
if (message) {
|
||||
srRegion.textContent = '';
|
||||
setTimeout(() => {
|
||||
srRegion.textContent = message + '\u00A0'; // add a non-breaking space
|
||||
srRegion.focus(); // Optional
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Exported for use in tests
|
||||
function initUploadStatusAnnouncer() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
announceUploadStatusFromElement();
|
||||
});
|
||||
}
|
||||
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
Modules.FileUpload = function() {
|
||||
|
||||
this.submit = () => this.$form.trigger('submit');
|
||||
|
||||
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
|
||||
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
|
||||
`);
|
||||
|
||||
this.start = function(component) {
|
||||
|
||||
this.$form = $(component);
|
||||
|
||||
// The label gets styled like a button and is used to hide the native file upload control. This is so that
|
||||
// users see a button that looks like the others on the site.
|
||||
|
||||
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
|
||||
|
||||
// Clear the form if the user navigates back to the page
|
||||
$(window).on("pageshow", () => this.$form[0].reset());
|
||||
|
||||
// Need to put the event on the container, not the input for it to work properly
|
||||
this.$form.on(
|
||||
'change', '.file-upload-field',
|
||||
() => this.submit() && this.showCancelButton()
|
||||
);
|
||||
|
||||
this.showCancelButton = () => {
|
||||
$('.file-upload-button', this.$form).replaceWith(`
|
||||
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
|
||||
Uploading<span class="dot-anim" aria-hidden="true"></span>
|
||||
</button>
|
||||
`);
|
||||
};
|
||||
|
||||
};
|
||||
this.start = function(component) {
|
||||
this.$form = $(component);
|
||||
|
||||
this.$form.on('click', '[data-module="upload-trigger"]', function () {
|
||||
const inputId = $(this).data('file-input-id');
|
||||
const fileInput = document.getElementById(inputId);
|
||||
if (fileInput) fileInput.click();
|
||||
});
|
||||
|
||||
$(window).on("pageshow", () => this.$form[0].reset());
|
||||
|
||||
this.$form.on('change', '.file-upload-field', () => {
|
||||
this.submit();
|
||||
this.showCancelButton();
|
||||
});
|
||||
};
|
||||
};
|
||||
})(window.GOVUK.Modules);
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
announceUploadStatusFromElement,
|
||||
initUploadStatusAnnouncer
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
initUploadStatusAnnouncer();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
this.$scrollableTable
|
||||
.on('scroll', this.toggleShadows)
|
||||
.on('scroll', this.maintainHeight)
|
||||
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
|
||||
|
||||
if (
|
||||
window.GOVUK.stickAtBottomWhenScrolling &&
|
||||
@@ -37,11 +36,11 @@
|
||||
|
||||
this.insertShims = () => {
|
||||
|
||||
const attributesForFocus = 'role aria-labelledby tabindex';
|
||||
const attributesForFocus = 'role aria-labelledby';
|
||||
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
|
||||
|
||||
this.$table.find('caption').attr('id', captionId);
|
||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`);
|
||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
|
||||
|
||||
this.$component
|
||||
.append(
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
(function(window) {
|
||||
"use strict";
|
||||
|
||||
window.GOVUK.Modules.Homepage = function() {
|
||||
|
||||
this.start = function(component) {
|
||||
|
||||
let $component = $(component);
|
||||
let iterations = 0;
|
||||
let timeout = null;
|
||||
|
||||
$component.on('click', () => {
|
||||
if (++iterations == 5) {
|
||||
$component.toggleClass('product-page-intro-wrapper--alternative');
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => iterations = 0, 1500);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
})(window);
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
let query = normalize($searchBox.val());
|
||||
let results = 0;
|
||||
let $noResultsMessage = $('.js-live-search-no-results');
|
||||
|
||||
$targets.each(function() {
|
||||
|
||||
@@ -39,6 +40,12 @@
|
||||
|
||||
});
|
||||
|
||||
if (query !== '' && results === 0) {
|
||||
$noResultsMessage.show();
|
||||
} else {
|
||||
$noResultsMessage.hide();
|
||||
}
|
||||
|
||||
if (state === 'loaded') {
|
||||
if (query !== '') {
|
||||
$searchBox.attr('aria-label', $searchLabel.text().trim() + ', ' + resultsSummary(results));
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
window.GOVUK.Frontend.initAll();
|
||||
|
||||
$(() => $("time.timeago").timeago());
|
||||
|
||||
var showHideContent = new GOVUK.ShowHideContent();
|
||||
showHideContent.init();
|
||||
|
||||
|
||||
100
app/assets/javascripts/notifyModal.js
Normal file
@@ -0,0 +1,100 @@
|
||||
let activeModal = null;
|
||||
let lastFocusedElement = null;
|
||||
|
||||
function openModal(modalId) {
|
||||
const wrapper = document.getElementById(modalId);
|
||||
if (!wrapper) return;
|
||||
|
||||
const modal = wrapper.querySelector('.usa-modal, dialog');
|
||||
if (!modal) return;
|
||||
|
||||
lastFocusedElement = document.activeElement;
|
||||
|
||||
wrapper.classList.remove('is-hidden');
|
||||
modal.removeAttribute('aria-hidden');
|
||||
modal.removeAttribute('inert');
|
||||
modal.removeAttribute('hidden');
|
||||
document.body.classList.add('modal-open');
|
||||
|
||||
|
||||
// Set focus to the first focusable element inside modal
|
||||
const focusTarget = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusTarget) focusTarget.focus();
|
||||
|
||||
modal.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
activeModal = wrapper;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (!activeModal) return;
|
||||
|
||||
const modal = activeModal.querySelector('.usa-modal, dialog');
|
||||
if (modal) {
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.setAttribute('inert', '');
|
||||
modal.setAttribute('hidden', '');
|
||||
}
|
||||
|
||||
activeModal.classList.add('is-hidden');
|
||||
document.body.classList.remove('modal-open');
|
||||
|
||||
if (lastFocusedElement) lastFocusedElement.focus();
|
||||
|
||||
activeModal = null;
|
||||
|
||||
}
|
||||
|
||||
function attachModalTriggers() {
|
||||
document.querySelectorAll('[data-open-modal]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const modalId = btn.getAttribute('data-open-modal');
|
||||
openModal(modalId);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-close-modal]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Escape key closes modal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && activeModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: click outside modal closes it
|
||||
document.addEventListener('click', (e) => {
|
||||
if (activeModal && e.target.classList.contains('usa-modal-overlay')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
attachModalTriggers();
|
||||
});
|
||||
|
||||
// ✅ 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 = { closeModal, openModal, attachModalTriggers };
|
||||
}
|
||||
@@ -1,37 +1,45 @@
|
||||
(function() {
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let disableSubmitButtons = function(event) {
|
||||
|
||||
var $submitButton = $(this).find(':submit');
|
||||
|
||||
if ($submitButton.data('clicked') == 'true') {
|
||||
const disableSubmitButtons = function (event) {
|
||||
const $submitButton = $(this).find(':submit');
|
||||
|
||||
if ($submitButton.data('clicked') === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
$submitButton.data('clicked', 'true');
|
||||
|
||||
$submitButton.data('clicked', 'true');
|
||||
// Add dot animation for Send/Schedule/Cancel buttons
|
||||
const buttonName = $submitButton.attr('name')?.toLowerCase();
|
||||
if (["send", "schedule", "cancel"].includes(buttonName)) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
if ($submitButton.is('[name="Send"], [name="Schedule"]')) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000);
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
// Inject dot animation span if not already present
|
||||
if ($submitButton.find('.dot-anim').length === 0) {
|
||||
$submitButton.append('<span class="dot-anim" aria-hidden="true"></span>');
|
||||
}
|
||||
|
||||
// Disable Cancel button too
|
||||
const $cancelButton = $('button[name]').filter(function () {
|
||||
return $(this).attr('name')?.toLowerCase() === 'cancel';
|
||||
});
|
||||
$cancelButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000); // fallback safety
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
}
|
||||
};
|
||||
|
||||
let renableSubmitButton = $submitButton => () => {
|
||||
|
||||
const renableSubmitButton = ($submitButton) => () => {
|
||||
$submitButton.data('clicked', '');
|
||||
$submitButton.prop('disabled', false);
|
||||
$submitButton.find('.dot-anim').remove(); // clean up if needed
|
||||
};
|
||||
|
||||
$('form').on('submit', disableSubmitButtons);
|
||||
|
||||
})();
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
|
||||
</div>
|
||||
{{/choices}}
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--done' aria-expanded='true' value='Done' />
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
|
||||
</div>
|
||||
`),
|
||||
'chosen': Hogan.compile(`
|
||||
|
||||
83
app/assets/javascripts/socketio.js
Normal file
@@ -0,0 +1,83 @@
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const isJobPage = window.location.pathname.includes('/jobs/');
|
||||
if (!isJobPage) return;
|
||||
|
||||
const jobEl = document.querySelector('[data-job-id]');
|
||||
const jobId = jobEl?.dataset?.jobId;
|
||||
const featureEnabled = jobEl?.dataset?.feature === 'true';
|
||||
const apiHost = jobEl?.dataset?.host;
|
||||
|
||||
if (!jobId) return;
|
||||
|
||||
if (featureEnabled) {
|
||||
const socket = io(apiHost);
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
console.error('Socket connect_error:', err);
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('join', { room: `job-${jobId}` });
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
socket.emit('leave', { room: `job-${jobId}` });
|
||||
});
|
||||
|
||||
const debouncedUpdate = debounce((data) => {
|
||||
updateAllJobSections();
|
||||
}, 1000);
|
||||
|
||||
socket.on('job_updated', (data) => {
|
||||
if (data.job_id !== jobId) return;
|
||||
debouncedUpdate(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllJobSections() {
|
||||
const resourceEl = document.querySelector('[data-socket-update="status"]');
|
||||
const url = resourceEl?.dataset?.resource;
|
||||
|
||||
if (!url) {
|
||||
console.warn('No resource URL found for job updates');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then((res) => res.json())
|
||||
.then(({ status, counts, notifications }) => {
|
||||
const sections = {
|
||||
status: document.querySelector('[data-socket-update="status"]'),
|
||||
counts: document.querySelector('[data-socket-update="counts"]'),
|
||||
notifications: document.querySelector(
|
||||
'[data-socket-update="notifications"]'
|
||||
),
|
||||
};
|
||||
|
||||
if (status && sections.status) {
|
||||
sections.status.innerHTML = status;
|
||||
}
|
||||
if (counts && sections.counts) {
|
||||
sections.counts.innerHTML = counts;
|
||||
}
|
||||
if (notifications && sections.notifications) {
|
||||
sections.notifications.innerHTML = notifications;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error fetching job update partials:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -38,6 +38,7 @@ function attachValidation() {
|
||||
const validatedRadioNames = new Set();
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.type === "hidden") return;
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
let errorElement = document.getElementById(errorId);
|
||||
|
||||
@@ -85,6 +86,7 @@ function attachValidation() {
|
||||
});
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.type === "hidden") return;
|
||||
input.addEventListener("input", function () {
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
const errorElement = document.getElementById(errorId);
|
||||
|
||||
8
app/assets/js/init.uswds.js
Normal file
@@ -0,0 +1,8 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.uswds && typeof window.uswds.init === 'function') {
|
||||
console.log("Calling USWDS init");
|
||||
window.uswds.init();
|
||||
} else {
|
||||
console.error("USWDS not found or init is not a function");
|
||||
}
|
||||
});
|
||||
BIN
app/assets/pdf/Studio Research & Development.pdf
Normal file
BIN
app/assets/pdf/Text Campaign Planning.pdf
Normal file
BIN
app/assets/pdf/The Notify Story.pdf
Normal file
BIN
app/assets/pdf/best-practices-for-texting-the-public.pdf
Normal file
BIN
app/assets/pdf/best-practices-section-outline.pdf
Normal file
BIN
app/assets/pdf/standing-up-your-own-notify.pdf
Normal file
BIN
app/assets/pdf/wa_case_study.pdf
Normal file
@@ -49,12 +49,12 @@
|
||||
}
|
||||
|
||||
.sms-message-sender, .sms-message-file-name, .sms-message-scheduler, .sms-message-template, .sms-message-sender {
|
||||
margin:0.25rem 0 0;
|
||||
margin: units(0.5) 0 0;
|
||||
}
|
||||
|
||||
.sms-message-recipient {
|
||||
color: color('gray-cool-90');
|
||||
margin: units(1) 0 units(1);
|
||||
margin: units(0.5) 0 units(2);
|
||||
}
|
||||
|
||||
.sms-message-status {
|
||||
@@ -344,3 +344,43 @@ h2.recipient-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usa-search__input, [type=search] {
|
||||
border-right: 1px solid;
|
||||
height: 40px;
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.usa-search .search-form__button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
// Button ellipses loading
|
||||
.dot-anim {
|
||||
display: inline-block;
|
||||
margin-left: 0; /* remove left margin if it exists */
|
||||
padding-left: 0;
|
||||
font-size: 1em;
|
||||
animation: dots 1s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
/* Optional: reduce spacing by removing whitespace node */
|
||||
button span.dot-anim {
|
||||
margin-left: 0; /* forces no space even if white-space exists */
|
||||
}
|
||||
|
||||
.dot-anim::after {
|
||||
content: '.';
|
||||
animation: dotPulse 1.5s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dotPulse {
|
||||
0% { content: ''; }
|
||||
33% { content: '.'; }
|
||||
66% { content: '..'; }
|
||||
100% { content: '...'; }
|
||||
}
|
||||
|
||||
.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -179,10 +179,19 @@ td.table-empty-message {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.user-list-item {
|
||||
border: 2px solid color('gray-5');
|
||||
padding: units(2);
|
||||
button, .usa-accordion__content {
|
||||
background-color: white;
|
||||
border: 2px solid color('gray-5');
|
||||
}
|
||||
button:hover, button[aria-expanded="true"] {
|
||||
background-color: color('gray-5');
|
||||
}
|
||||
@include at-media(desktop) {
|
||||
width: calc(50% - units(1));
|
||||
margin-bottom: 1rem;
|
||||
@@ -199,7 +208,6 @@ td.table-empty-message {
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: size('body', 'sm');
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -289,6 +297,7 @@ td.table-empty-message {
|
||||
margin-top: units(1);
|
||||
width: 100%;
|
||||
border: 1px solid color('gray-60');
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,11 +600,25 @@ td.table-empty-message {
|
||||
.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:hover {
|
||||
color: color('blue-60v');
|
||||
&.pill-item--selected {
|
||||
.pill-item__label {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
&:hover {
|
||||
.pill-item__label {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@forward "uswds-theme";
|
||||
@forward "uswds";
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@forward "legacy-styles";
|
||||
@forward "main";
|
||||
@forward "data-visualization";
|
||||
|
||||
@@ -37,6 +37,7 @@ $path: '/static/images/';
|
||||
// Custom overrides
|
||||
.govuk-link {
|
||||
font-weight: bold;
|
||||
color: #005ea5;
|
||||
}
|
||||
|
||||
// Specific to this application
|
||||
|
||||
@@ -12,6 +12,9 @@ class Config(object):
|
||||
NOTIFY_APP_NAME = "admin"
|
||||
NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development")
|
||||
API_HOST_NAME = getenv("API_HOST_NAME", "localhost")
|
||||
API_PUBLIC_URL = getenv("API_PUBLIC_URL", "localhost")
|
||||
API_PUBLIC_WS_URL = getenv("API_PUBLIC_WS_URL", "localhost")
|
||||
|
||||
ADMIN_BASE_URL = getenv("ADMIN_BASE_URL", "http://localhost:6012")
|
||||
HEADER_COLOUR = (
|
||||
"#81878b" # mix(govuk-colour("dark-grey"), govuk-colour("mid-grey"))
|
||||
@@ -88,6 +91,8 @@ class Config(object):
|
||||
],
|
||||
}
|
||||
|
||||
FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "false") == "true"
|
||||
|
||||
|
||||
def _s3_credentials_from_env(bucket_prefix):
|
||||
return {
|
||||
@@ -128,8 +133,9 @@ class Test(Development):
|
||||
WTF_CSRF_ENABLED = False
|
||||
ASSET_DOMAIN = "static.example.com"
|
||||
ASSET_PATH = "https://static.example.com/"
|
||||
|
||||
API_HOST_NAME = "http://you-forgot-to-mock-an-api-call-to"
|
||||
API_PUBLIC_URL = "http://you-forgot-to-mock-an-api-call-to"
|
||||
API_PUBLIC_WS_URL = "ws://you-forgot-to-mock-an-api-call-to"
|
||||
REDIS_URL = "redis://you-forgot-to-mock-a-redis-call-to"
|
||||
LOGO_CDN_DOMAIN = "static-logos.test.com"
|
||||
|
||||
|
||||
@@ -344,7 +344,7 @@ def nl2br(value):
|
||||
html="escape",
|
||||
)
|
||||
).then(utils_nl2br)
|
||||
)
|
||||
) # nosec
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ def govuk_text_input_field_widget(
|
||||
|
||||
return Markup(
|
||||
render_template("components/components/input/template.njk", params=params)
|
||||
)
|
||||
) # nosec
|
||||
|
||||
|
||||
class GovukTextInputField(StringField):
|
||||
@@ -690,7 +690,9 @@ def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
if param_extensions:
|
||||
merge_jsonlike(params, param_extensions)
|
||||
|
||||
return Markup(render_template("forms/fields/checkboxes/macro.njk", params=params))
|
||||
return Markup(
|
||||
render_template("forms/fields/checkboxes/macro.njk", params=params)
|
||||
) # nosec
|
||||
|
||||
|
||||
def govuk_checkboxes_field_widget(
|
||||
@@ -704,7 +706,7 @@ def govuk_checkboxes_field_widget(
|
||||
f' data-field-label="{field_label}">'
|
||||
f" {checkboxes_string}"
|
||||
f"</div>"
|
||||
)
|
||||
) # nosec
|
||||
|
||||
return result
|
||||
|
||||
@@ -757,12 +759,14 @@ def govuk_checkboxes_field_widget(
|
||||
|
||||
return _wrap_in_collapsible(
|
||||
self.field_label,
|
||||
Markup(render_template("forms/fields/checkboxes/macro.njk", params=params)),
|
||||
Markup(
|
||||
render_template("forms/fields/checkboxes/macro.njk", params=params)
|
||||
), # nosec
|
||||
)
|
||||
else:
|
||||
return Markup(
|
||||
render_template("forms/fields/checkboxes/macro.njk", params=params)
|
||||
)
|
||||
) # nosec
|
||||
|
||||
|
||||
def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
@@ -805,7 +809,7 @@ def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
|
||||
return Markup(
|
||||
render_template("components/components/radios/template.njk", params=params)
|
||||
)
|
||||
) # nosec
|
||||
|
||||
|
||||
class GovukCheckboxField(BooleanField):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
@@ -19,6 +21,8 @@ from app.main.views.sub_navigation_dictionaries import (
|
||||
)
|
||||
from app.utils.user import user_is_logged_in
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Hook to check for feature flags
|
||||
@main.before_request
|
||||
@@ -33,7 +37,7 @@ def check_feature_flags():
|
||||
@main.route("/test/feature-flags")
|
||||
def test_feature_flags():
|
||||
return jsonify(
|
||||
{"FEATURE_ABOUT_PAGE_ENABLED": current_app.config["FEATURE_ABOUT_PAGE_ENABLED"]}
|
||||
{"FEATURE_SOCKET_ENABLED": current_app.config["FEATURE_SOCKET_ENABLED"]}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def accept_invite(token):
|
||||
<a href={url_for("main.sign_out")} class="usa-link">Sign out</a>
|
||||
and click the link again to accept this invite.
|
||||
"""
|
||||
)
|
||||
) # nosec
|
||||
|
||||
flash(message=message)
|
||||
|
||||
@@ -98,7 +98,7 @@ def accept_org_invite(token):
|
||||
""".format(
|
||||
current_user.email_address, url_for("main.sign_out")
|
||||
)
|
||||
)
|
||||
) # nosec
|
||||
|
||||
flash(message=message)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
@@ -57,8 +58,12 @@ def view_job(service_id, job_id):
|
||||
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
api_public_url = os.environ.get("API_PUBLIC_URL")
|
||||
|
||||
return render_template(
|
||||
"views/jobs/job.html",
|
||||
api_public_url=api_public_url,
|
||||
FEATURE_SOCKET_ENABLED=current_app.config["FEATURE_SOCKET_ENABLED"],
|
||||
job=job,
|
||||
status=request.args.get("status", ""),
|
||||
updates_url=url_for(
|
||||
@@ -351,7 +356,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""total<span class="usa-sr-only">
|
||||
{"text message" if job_type == "sms" else job_type}s</span>"""
|
||||
),
|
||||
), # nosec
|
||||
"",
|
||||
job.notification_count,
|
||||
],
|
||||
@@ -359,7 +364,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""pending<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_sending, job_type)}</span>"""
|
||||
),
|
||||
), # nosec
|
||||
"pending",
|
||||
job.notifications_sending,
|
||||
],
|
||||
@@ -367,7 +372,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""delivered<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_delivered, job_type)}</span>"""
|
||||
),
|
||||
), # nosec
|
||||
"delivered",
|
||||
job.notifications_delivered,
|
||||
],
|
||||
@@ -375,7 +380,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""failed<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_failed, job_type)}</span>"""
|
||||
),
|
||||
), # nosec
|
||||
"failed",
|
||||
job.notifications_failed,
|
||||
],
|
||||
@@ -465,4 +470,4 @@ def get_preview_of_content(notification):
|
||||
notification["personalisation"],
|
||||
redact_missing_personalisation=True,
|
||||
).subject
|
||||
)
|
||||
) # nosec
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuid
|
||||
from string import ascii_uppercase
|
||||
from zipfile import BadZipFile
|
||||
|
||||
import bleach
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
@@ -170,7 +171,9 @@ def send_messages(service_id, template_id):
|
||||
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)
|
||||
# use 'nosec' because we applied bleach.clean to strip dangerous tags
|
||||
error_message = bleach.clean(error_message)
|
||||
error_message = Markup(error_message) # nosec
|
||||
flash(error_message)
|
||||
column_headings = get_spreadsheet_column_headings_from_template(template)
|
||||
|
||||
@@ -478,6 +481,7 @@ def send_one_off_step(service_id, template_id, step_index):
|
||||
),
|
||||
template=template,
|
||||
form=form,
|
||||
current_placeholder=current_placeholder,
|
||||
skip_link=get_skip_link(step_index, template),
|
||||
back_link=back_link,
|
||||
link_to_upload=(
|
||||
@@ -557,24 +561,47 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
|
||||
)
|
||||
|
||||
if request.args.get("from_test"):
|
||||
# TODO: may not be required after letters code removed
|
||||
back_link = url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
)
|
||||
back_link_from_preview = url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
)
|
||||
back_link = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
),
|
||||
"text": "Back to message personalization",
|
||||
},
|
||||
"html": "Back to message personalization",
|
||||
}
|
||||
back_link_from_preview = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
),
|
||||
"text": "Back to message personalization",
|
||||
},
|
||||
"html": "Back to message personalization",
|
||||
}
|
||||
choose_time_form = None
|
||||
else:
|
||||
back_link = url_for(
|
||||
"main.send_messages", service_id=service_id, template_id=template.id
|
||||
)
|
||||
back_link_from_preview = url_for(
|
||||
"main.check_messages",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
upload_id=upload_id,
|
||||
)
|
||||
back_link = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_messages", service_id=service_id, template_id=template.id
|
||||
),
|
||||
"text": "Back to upload a file",
|
||||
},
|
||||
"html": "Back to upload a file",
|
||||
}
|
||||
back_link_from_preview = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.check_messages",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
upload_id=upload_id,
|
||||
),
|
||||
"text": "Back to check messages",
|
||||
},
|
||||
"html": "Back to check messages",
|
||||
}
|
||||
choose_time_form = ChooseTimeForm()
|
||||
|
||||
if preview_row < 2:
|
||||
@@ -752,31 +779,62 @@ def get_back_link(
|
||||
preview=False,
|
||||
):
|
||||
if preview:
|
||||
return url_for(
|
||||
"main.check_notification",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
)
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.check_notification",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
),
|
||||
"text": "Back to select delivery time",
|
||||
},
|
||||
"html": "Back to select delivery time",
|
||||
}
|
||||
|
||||
if step_index == 0:
|
||||
if should_skip_template_page(template._template):
|
||||
return url_for(
|
||||
".choose_template",
|
||||
service_id=service_id,
|
||||
)
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
".choose_template",
|
||||
service_id=service_id,
|
||||
),
|
||||
"text": "Back to all templates",
|
||||
},
|
||||
"html": "Back to all templates",
|
||||
}
|
||||
else:
|
||||
return url_for(
|
||||
".view_template",
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
".view_template",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
),
|
||||
"text": "Back to confirm your template",
|
||||
},
|
||||
"html": "Back to confirm your template",
|
||||
}
|
||||
|
||||
# fallback for other steps
|
||||
back_to_text = (
|
||||
"Back to select recipients"
|
||||
if step_index == 1
|
||||
else "Back to message personalization"
|
||||
)
|
||||
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_one_off_step",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
)
|
||||
|
||||
return url_for(
|
||||
"main.send_one_off_step",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
step_index=step_index - 1,
|
||||
)
|
||||
step_index=step_index - 1,
|
||||
),
|
||||
"text": back_to_text,
|
||||
},
|
||||
"html": back_to_text,
|
||||
}
|
||||
|
||||
|
||||
def get_skip_link(step_index, template):
|
||||
@@ -876,7 +934,7 @@ def _check_notification(service_id, template_id, exception=None, **kwargs):
|
||||
if (not session.get("recipient")) or not all_placeholders_in_session(
|
||||
template.placeholders
|
||||
):
|
||||
raise PermanentRedirect(back_link)
|
||||
raise PermanentRedirect(back_link["href"]["url"])
|
||||
|
||||
template.values = get_recipient_and_placeholders_from_session(
|
||||
template.template_type
|
||||
|
||||
@@ -62,7 +62,7 @@ def _get_access_token(code): # pragma: no cover
|
||||
code_param = f"code={code}"
|
||||
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
||||
headers = {"Authorization": "Bearer %s" % token}
|
||||
response = requests.post(url, headers=headers)
|
||||
response = requests.post(url, headers=headers, timeout=30)
|
||||
|
||||
response_json = response.json()
|
||||
id_token = get_id_token(response_json)
|
||||
@@ -88,10 +88,7 @@ def _get_access_token(code): # pragma: no cover
|
||||
def _get_user_email_and_uuid(access_token): # pragma: no cover
|
||||
headers = {"Authorization": "Bearer %s" % access_token}
|
||||
user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL")
|
||||
user_attributes = requests.get(
|
||||
user_info_url,
|
||||
headers=headers,
|
||||
)
|
||||
user_attributes = requests.get(user_info_url, headers=headers, timeout=30)
|
||||
user_email = user_attributes.json()["email"]
|
||||
user_uuid = user_attributes.json()["sub"]
|
||||
return user_email, user_uuid
|
||||
|
||||
@@ -17,7 +17,7 @@ def _sign_out_at_login_dot_gov():
|
||||
url = f"{base_url}{client_id}&{post_logout_redirect_uri}"
|
||||
current_app.logger.info(f"url={url}")
|
||||
|
||||
response = requests.post(url)
|
||||
response = requests.post(url, timeout=30)
|
||||
|
||||
# response = requests.post(url)
|
||||
current_app.logger.info(f"login.gov response: {response.text}")
|
||||
|
||||
@@ -698,9 +698,7 @@ def _get_content_count_error_and_message_for_template(template):
|
||||
# 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..."
|
||||
return False, Markup(warning) # nosec
|
||||
|
||||
# If message is too long, return the length error
|
||||
if template.is_message_too_long():
|
||||
@@ -715,11 +713,11 @@ def _get_content_count_error_and_message_for_template(template):
|
||||
return False, Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
f"(not including personalization)."
|
||||
)
|
||||
) # nosec
|
||||
|
||||
return False, Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)}."
|
||||
)
|
||||
) # nosec
|
||||
|
||||
|
||||
@main.route(
|
||||
|
||||
@@ -139,14 +139,28 @@ def tour_step(service_id, template_id, step_index):
|
||||
|
||||
def _get_tour_step_back_link(service_id, template_id, step_index):
|
||||
if step_index == 1:
|
||||
return url_for(".begin_tour", service_id=service_id, template_id=template_id)
|
||||
|
||||
return url_for(
|
||||
".tour_step",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=step_index - 1,
|
||||
)
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.begin_tour", service_id=service_id, template_id=template_id
|
||||
),
|
||||
"text": "Back to tour start",
|
||||
},
|
||||
"html": "Back to tour start",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.tour_step",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=step_index - 1,
|
||||
),
|
||||
"text": "Back to previous step",
|
||||
},
|
||||
"html": "Back to previous step",
|
||||
}
|
||||
|
||||
|
||||
@main.route(
|
||||
@@ -183,12 +197,18 @@ def check_tour_notification(service_id, template_id):
|
||||
)
|
||||
)
|
||||
|
||||
back_link = url_for(
|
||||
".tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=len(placeholders),
|
||||
)
|
||||
back_link = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=len(placeholders),
|
||||
),
|
||||
"text": "Back to previous step",
|
||||
},
|
||||
"html": "Back to previous step",
|
||||
}
|
||||
|
||||
template.values = get_recipient_and_placeholders_from_session(
|
||||
template.template_type
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from app.extensions import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
|
||||
@@ -81,7 +83,13 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
"""
|
||||
Retrieve a list of live services data with contact names and notification counts.
|
||||
"""
|
||||
return self.get("/service/live-services-data", params=params_dict)
|
||||
try:
|
||||
return self.get("/service/live-services-data", params=params_dict)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
f"Error getting live services data {str(e)}", exc_info=True
|
||||
)
|
||||
raise e
|
||||
|
||||
def get_active_services(self, params_dict=None):
|
||||
"""
|
||||
@@ -136,19 +144,29 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
|
||||
@cache.delete("live-service-and-organization-counts")
|
||||
def update_status(self, service_id, live):
|
||||
return self.update_service(
|
||||
service_id,
|
||||
message_limit=250000 if live else 50,
|
||||
restricted=(not live),
|
||||
go_live_at=str(datetime.utcnow()) if live else None,
|
||||
)
|
||||
try:
|
||||
return self.update_service(
|
||||
service_id,
|
||||
message_limit=250000 if live else 50,
|
||||
restricted=(not live),
|
||||
go_live_at=str(datetime.utcnow()) if live else None,
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error updating status {str(e)}", exc_info=True)
|
||||
raise e
|
||||
|
||||
@cache.delete("live-service-and-organization-counts")
|
||||
def update_count_as_live(self, service_id, count_as_live):
|
||||
return self.update_service(
|
||||
service_id,
|
||||
count_as_live=count_as_live,
|
||||
)
|
||||
try:
|
||||
return self.update_service(
|
||||
service_id,
|
||||
count_as_live=count_as_live,
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
f"Error updating count as live {str(e)}", exc_info=True
|
||||
)
|
||||
raise e
|
||||
|
||||
# This method is not cached because it calls through to one which is
|
||||
def update_service_with_properties(self, service_id, properties):
|
||||
|
||||
@@ -28,8 +28,17 @@ def get_csv_upload(service_id, upload_id):
|
||||
return get_s3_object(*get_csv_location(service_id, upload_id))
|
||||
|
||||
|
||||
def remove_blank_lines(filedata):
|
||||
# sometimes people upload files with hundreds of blank lines at the end
|
||||
data = filedata["data"]
|
||||
cleaned_data = "\r\n".join(line for line in data.splitlines() if line.strip())
|
||||
filedata["data"] = cleaned_data
|
||||
return filedata
|
||||
|
||||
|
||||
def s3upload(service_id, filedata):
|
||||
|
||||
filedata = remove_blank_lines(filedata)
|
||||
upload_id = str(uuid.uuid4())
|
||||
bucket_name, file_location, access_key, secret_key, region = get_csv_location(
|
||||
service_id, upload_id
|
||||
|
||||
@@ -11,10 +11,16 @@
|
||||
{% include "components/head.html" %}
|
||||
</head>
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
<div id="upload-status-live"
|
||||
class="usa-sr-only"
|
||||
aria-live="assertive"
|
||||
tabindex="-1"
|
||||
role="status">
|
||||
</div>
|
||||
<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 %}
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
|
||||
<noscript><iframe sandbox src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -29,35 +35,37 @@
|
||||
|
||||
{% 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>
|
||||
{% if not is_api_down %}
|
||||
{% 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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% include 'components/header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-container minh-mobile">
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -132,7 +140,7 @@
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timeLeft">
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="sessionTimerHeading">
|
||||
<h2 class="usa-modal__heading font-body-lg" id="sessionTimerHeading">
|
||||
Your session will end soon.
|
||||
<span class="usa-sr-only">Please choose to extend your session or sign out. Your session will expire in 5 minutes or less.</span>
|
||||
</h2>
|
||||
@@ -168,7 +176,7 @@
|
||||
{% block extra_javascripts %}
|
||||
{% endblock %}
|
||||
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
<script src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None, thing=None) %}
|
||||
<div
|
||||
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
|
||||
{% if type == 'dangerous' %}
|
||||
role='group'
|
||||
tabindex='-1'
|
||||
{% endif %}
|
||||
{% if id %}
|
||||
id={{ id }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{% macro usaBackLink(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{%- include "./template.njk" with context %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<nav class="usa-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="{{ params.href.url or params.href or '#' }}" class="usa-link usa-back-link display-inline-flex {{ params.classes or '' }}"
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {% endfor %}>{{ params.html | safe or params.href.text or 'Back' }}</a>
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {% endfor %}>
|
||||
{{- params.html | safe or params.href.text or 'Back' -}}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{# Determine type of element to use, if not explicitly set -#}
|
||||
|
||||
{% if params.element %}
|
||||
{% set element = params.element | lower %}
|
||||
{% else %}
|
||||
@@ -10,26 +9,35 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{#- Define common attributes that we can use across all element types #}
|
||||
|
||||
{# Define common attributes to use across all element types -#}
|
||||
{%- set commonAttributes %} class="usa-button{% if params.classes %} {{ params.classes }}{% endif %}{% if params.disabled %} usa-button--disabled{% endif %}"{% for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}{% endset %}
|
||||
|
||||
{#- Define common attributes we can use for both button and input types #}
|
||||
|
||||
{# Define attributes for button/input -#}
|
||||
{%- 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! #}
|
||||
|
||||
{%- if element == 'a' %}
|
||||
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{- commonAttributes | safe }}>
|
||||
{# Auto-append .dot-anim span for Send/Schedule buttons -#}
|
||||
{%- set isSendOrSchedule = params.name and (params.name | lower == 'send' or params.name | lower == 'schedule') -%}
|
||||
{%- set textContent %}
|
||||
{{ params.html | safe if params.html else params.text }}
|
||||
</a>
|
||||
{%- if isSendOrSchedule -%}<span class="dot-anim" aria-hidden="true"></span>{%- endif -%}
|
||||
{%- endset %}
|
||||
|
||||
{%- elseif element == 'button' %}
|
||||
<button {%- if params.value %} value="{{ params.value }}"{% endif %} {{- buttonAttributes | safe }} {{- commonAttributes | safe }} {% if params.disabled %}disabled{% endif %}>
|
||||
{{ params.html | safe if params.html else params.text }}
|
||||
</button>
|
||||
{# Render the appropriate element -#}
|
||||
{% if element == 'a' %}
|
||||
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{ commonAttributes | safe }}>
|
||||
{{ textContent | safe }}
|
||||
</a>
|
||||
|
||||
{%- elseif element == 'input' %}
|
||||
<input value="{{ params.text }}" {{- buttonAttributes | safe }} {{- commonAttributes | safe }}>
|
||||
{%- endif %}
|
||||
{% elseif element == 'button' %}
|
||||
<button
|
||||
{% if params.value %} value="{{ params.value }}"{% endif %}
|
||||
{{ buttonAttributes | safe }}
|
||||
{{ commonAttributes | safe }}
|
||||
{% if params.disabled %}disabled{% endif %}
|
||||
>
|
||||
{{ textContent | safe }}
|
||||
</button>
|
||||
|
||||
{% elseif element == 'input' %}
|
||||
<input value="{{ params.text }}" {{ buttonAttributes | safe }} {{ commonAttributes | safe }}>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
{% from "../hint/macro.njk" import usaHint %}
|
||||
{% from "../label/macro.njk" import usaLabel %}
|
||||
|
||||
{#- a record of other elements that we need to associate with the input using
|
||||
aria-describedby – for example hints or error messages -#}
|
||||
{# Generate a consistent input ID #}
|
||||
{% set inputId = params.id if params.id else params.label.text | default('unknown') | slugify %}
|
||||
{% set describedBy = params.describedBy if params.describedBy else "" %}
|
||||
|
||||
<div class="usa-form-group {%- if params.errorMessage %} usa-form-group--error{% endif %} {%- if params.formGroup.classes %} {{ params.formGroup.classes }}{% endif %}">
|
||||
{{ usaLabel({
|
||||
html: params.label.html,
|
||||
@@ -12,38 +13,43 @@
|
||||
classes: params.label.classes,
|
||||
isPageHeading: params.label.isPageHeading,
|
||||
attributes: params.label.attributes,
|
||||
for: params.text
|
||||
for: inputId
|
||||
}) | indent(2) | trim }}
|
||||
{% if params.hint %}
|
||||
{% set hintId = params.id + '-hint' %}
|
||||
{% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
|
||||
{{ usaHint({
|
||||
id: hintId,
|
||||
classes: params.hint.classes,
|
||||
attributes: params.hint.attributes,
|
||||
html: params.hint.html,
|
||||
text: params.hint.text
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
{% if params.errorMessage %}
|
||||
{% set errorId = params.label.text + '-error' %}
|
||||
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
||||
{{ usaErrorMessage({
|
||||
id: errorId,
|
||||
classes: params.errorMessage.classes,
|
||||
attributes: params.errorMessage.attributes,
|
||||
html: params.errorMessage.html,
|
||||
text: params.errorMessage.text,
|
||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
|
||||
{% if params.hint %}
|
||||
{% set hintId = inputId + '-hint' %}
|
||||
{% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
|
||||
{{ usaHint({
|
||||
id: hintId,
|
||||
classes: params.hint.classes,
|
||||
attributes: params.hint.attributes,
|
||||
html: params.hint.html,
|
||||
text: params.hint.text
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
|
||||
{% if params.errorMessage %}
|
||||
{% set errorId = inputId + '-error' %}
|
||||
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
||||
{{ usaErrorMessage({
|
||||
id: errorId,
|
||||
classes: params.errorMessage.classes,
|
||||
attributes: params.errorMessage.attributes,
|
||||
html: params.errorMessage.html,
|
||||
text: params.errorMessage.text,
|
||||
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.label.text | default('unknown') | slugify }}"
|
||||
class="usa-input{%- if params.classes %} {{ params.classes }}{% endif %}{%- if params.errorMessage %} usa-input--error{% endif %}"
|
||||
id="{{ inputId }}"
|
||||
name="{{ params.name }}"
|
||||
type="{{ params.type | default('text') }}"
|
||||
{%- if params.value %} value="{{ params.value }}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy | trim }}"{% endif %}
|
||||
{# Uncomment below if you want to use aria-labelledby too
|
||||
aria-labelledby="{{ inputId }}-label" #}
|
||||
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete }}"{% endif %}
|
||||
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}
|
||||
|
||||
@@ -27,17 +27,21 @@
|
||||
</label>
|
||||
{{ field(**{
|
||||
'class': 'file-upload-field',
|
||||
'id': field.name,
|
||||
'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e
|
||||
}) }}
|
||||
<label class="file-upload-button" for="{{ field.name }}">
|
||||
{{ button_text }}
|
||||
</label>
|
||||
<button type="button"
|
||||
class="usa-button file-upload-button"
|
||||
data-module="upload-trigger"
|
||||
data-file-input-id="{{ field.name }}">
|
||||
{{ button_text }}
|
||||
</button>
|
||||
{% if alternate_link and alternate_link_text %}
|
||||
<span class="file-upload-alternate-link">
|
||||
or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<label class="file-upload-filename" for="{{ field.name }}"></label>
|
||||
<span class="file-upload-filename" aria-live="polite"></span>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }}
|
||||
</form>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
show=False,
|
||||
form=None,
|
||||
label=None,
|
||||
autofocus=False
|
||||
autofocus=True
|
||||
) %}
|
||||
{%- set search_label = label or form.search.label.text %}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
} %}
|
||||
|
||||
{% if autofocus %}
|
||||
{% set x=param_extensions.__setitem__("attributes", {"data-module": "autofocus"}) %}
|
||||
{% set _ = param_extensions.__setitem__("attributes", {"autofocus": "autofocus"}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if show %}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
highlight_placeholders=False,
|
||||
autofocus=False,
|
||||
autosize=False,
|
||||
colour_preview=False,
|
||||
help_link=None,
|
||||
help_link_text=None,
|
||||
width='2-3',
|
||||
@@ -13,58 +12,82 @@
|
||||
safe_error_message=False,
|
||||
rows=8,
|
||||
extra_form_group_classes='',
|
||||
placeholder=''
|
||||
placeholder='',
|
||||
input_id=None
|
||||
) %}
|
||||
{% set field_id = kwargs.id if kwargs.id is defined else field.id %}
|
||||
{% set described_by = '' %}
|
||||
{% if hint %}{% set described_by = described_by + field_id ~ '-hint' %}{% endif %}
|
||||
{% if field.errors %}
|
||||
{% if described_by %}{% set described_by = described_by + ' ' %}{% endif %}
|
||||
{% set described_by = described_by + field_id ~ '-error' %}
|
||||
{% endif %}
|
||||
|
||||
<div
|
||||
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
|
||||
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
|
||||
>
|
||||
<label class="usa-label" for="{{ field.name }}">
|
||||
{% if label %}
|
||||
{{ label }}
|
||||
{% else %}
|
||||
{{ field.label.text }}
|
||||
{% endif %}
|
||||
<label class="usa-label" for="{{ field_id }}">
|
||||
{{ label or field.label.text }}
|
||||
</label>
|
||||
|
||||
{% if hint %}
|
||||
<div class="usa-hint">
|
||||
<div class="usa-hint" id="{{ field_id }}-hint">
|
||||
{{ hint }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<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 id="{{ field_id }}-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 %}
|
||||
{% if not safe_error_message %}
|
||||
{{ field.errors[0] }}
|
||||
{% else %}
|
||||
{{ field.errors[0] | safe }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{%
|
||||
if highlight_placeholders or autosize
|
||||
%}
|
||||
|
||||
{% if highlight_placeholders or autosize %}
|
||||
{% set field_class = 'usa-textarea form-control-{} textbox-highlight-textbox'.format(width) %}
|
||||
{% else %}
|
||||
{% set field_class = 'form-control-{} {}'.format(width, 'textbox-right-aligned' if suffix else '') %}
|
||||
{% set field_class = 'usa-input' %}
|
||||
{% endif %}
|
||||
{%
|
||||
set field_class = 'form-control ' + field_class + (
|
||||
' form-control-error' if field.errors else ''
|
||||
)
|
||||
%}
|
||||
{{ field(
|
||||
class=field_class,
|
||||
data_module='enhanced-textbox' if highlight_placeholders or autosize else '',
|
||||
data_highlight_placeholders='true' if highlight_placeholders else 'false',
|
||||
rows=rows|string,
|
||||
placeholder=placeholder,
|
||||
aria_describedby=field.name+"-error",
|
||||
required='required' if required else None,
|
||||
**kwargs
|
||||
) }}
|
||||
{% set field_class = 'form-control ' ~ field_class ~ (' form-control-error' if field.errors else '') %}
|
||||
|
||||
{% if autosize or highlight_placeholders %}
|
||||
<textarea
|
||||
id="{{ field_id }}"
|
||||
name="{{ field.name }}"
|
||||
class="{{ field_class }}"
|
||||
rows="{{ rows }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-describedby="{{ described_by.strip() }}"
|
||||
{% if field.flags.required %}required{% endif %}
|
||||
data-module="enhanced-textbox"
|
||||
data-highlight-placeholders="true"
|
||||
>{{ field._value() }}</textarea>
|
||||
{% else %}
|
||||
{% set input_type = 'tel' if 'phone' in field.label.text else 'text' %}
|
||||
<input
|
||||
id="{{ field_id }}"
|
||||
name="{{ field.name }}"
|
||||
type="{{ input_type }}"
|
||||
class="{{ field_class }}"
|
||||
value="{{ field._value() }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-describedby="{{ described_by.strip() }}"
|
||||
{% if field.flags.required %}required{% endif %}
|
||||
/>
|
||||
{% endif %}
|
||||
|
||||
{% if suffix %}
|
||||
<span>{{ suffix }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if help_link and help_link_text %}
|
||||
<p class="textbox-help-link">
|
||||
<a class="usa-link" href='{{ help_link }}'>{{ help_link_text }}</a>
|
||||
<a class="usa-link" href="{{ help_link }}">{{ help_link_text }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
17
app/templates/error/technical_difficulties.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<section class="grid-container usa-section margin-top-5 margin-bottom-5">
|
||||
<div class="grid-row flex-justify-center">
|
||||
<div class="desktop:grid-col-3 tablet:grid-col-4 mobile:grid-col-12 flex-align-center">
|
||||
<img
|
||||
class="width-tablet tablet:width-full"
|
||||
src="{{ asset_url('images/api-error.svg') }}"
|
||||
alt="Mobile phone showing error screen"/>
|
||||
</div>
|
||||
<div class="desktop:grid-col-6 tablet:grid-col-12 padding-left-0 desktop:padding-left-5">
|
||||
<h1 class="font-body-3xl line-height-sans-2 text-bold">
|
||||
There's currently a technical issue.
|
||||
</h1>
|
||||
<p class="font-body-lg line-height-sans-2">Thank you for your patience while we work on it. Notify will be back soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -16,7 +16,10 @@
|
||||
<li>More effectively deliver program outcomes</li>
|
||||
<li>Save administrative costs</li>
|
||||
<li>Implement <a class="use-link usa-link--external"
|
||||
href="https://digital.gov/resources/delivering-digital-first-public-experience/" target="_blank">21st Century
|
||||
href="https://digital.gov/resources/delivering-digital-first-public-experience/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="21st Century IDEA (opens in a new tab)">21st Century
|
||||
IDEA</a> and other directives</li>
|
||||
</ul>
|
||||
<p>Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users
|
||||
|
||||
@@ -22,16 +22,26 @@
|
||||
</ul>
|
||||
<p>
|
||||
Notify.gov operates under a full three-year <a class="use-link usa-link--external"
|
||||
href="https://digital.gov/resources/an-introduction-to-ato/" target="_blank">Authority-to-Operate (ATO)</a>. This
|
||||
href="https://digital.gov/resources/an-introduction-to-ato/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Authority-to-Operate (ATO) (opens in a new tab)">Authority-to-Operate (ATO)</a>. This
|
||||
federal security authorization process leverages security
|
||||
controls provided by National Institute of Standards and Technology (NIST).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Our infrastructure runs on <a class="use-link usa-link--external" href="https://www.cloud.gov/"
|
||||
target="_blank">cloud.gov</a> and utilizes several
|
||||
Our infrastructure runs on <a class="use-link usa-link--external"
|
||||
href="https://www.cloud.gov/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="cloud dot gov (opens in a new tab)">cloud.gov</a> and utilizes several
|
||||
services through Amazon Web
|
||||
Services (AWS), including <a class="use-link usa-link--external" href="https://aws.amazon.com/sns/" target="_blank">
|
||||
Services (AWS), including <a class="use-link usa-link--external"
|
||||
href="https://aws.amazon.com/sns/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="AWS SNS (opens in a new tab)">
|
||||
AWS SNS </a> for sending SMS
|
||||
messages.
|
||||
</p>
|
||||
@@ -53,15 +63,22 @@
|
||||
</p>
|
||||
<h2>Multi-Factor Authentication</h2>
|
||||
<p>
|
||||
Notify.gov uses <a class="use-link usa-link--external" href="https://login.gov/what-is-login/"
|
||||
target="_blank">Login.gov</a> for enhanced security.
|
||||
Notify.gov uses <a class="use-link usa-link--external"
|
||||
href="https://login.gov/what-is-login/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="Login dot gov (opens in a new tab)">Login.gov</a> for enhanced security.
|
||||
Login.gov is an extra layer of security created by the government that uses multi-factor authentication and stronger
|
||||
passwords to protect your account.
|
||||
</p>
|
||||
<p>
|
||||
To access Notify.gov, users will use a Login.gov account associated with their agency (.gov) email with one of the
|
||||
<a class="use-link usa-link--external" href="https://login.gov/help/get-started/authentication-methods/"
|
||||
target="_blank">multi-factor authentication
|
||||
<a class="use-link usa-link--external"
|
||||
href="https://login.gov/help/get-started/authentication-methods/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="login dot gov multi-factor authentication methods (opens in a new tab)"
|
||||
>multi-factor authentication
|
||||
methods</a> offered through Login.gov.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
<p>
|
||||
Confusing or <a class="use-link usa-link--external"
|
||||
href="https://kffhealthnews.org/news/tougher-returned-mail-policies-add-to-medicaid-enrollment-drop/"
|
||||
target="_blank">unreceived notifications</a> are one of the largest barriers to people getting and keeping
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="unreceived notifications (opens in a new tab)">unreceived notifications</a> are one of the largest barriers to people getting and keeping
|
||||
benefits. The typical ways the government communicates with people often fall short. Low income households are more
|
||||
likely to experience housing instability, which means paper mail, already slow, can easily be missed.
|
||||
</p>
|
||||
@@ -24,7 +26,9 @@
|
||||
<p>
|
||||
<a class="use-link usa-link--external"
|
||||
href="https://www.pewresearch.org/internet/fact-sheet/mobile/?tabItem=64e32376-5a21-4b1d-8f8b-5f92406db984"
|
||||
target="_blank">Pew Research shows that nearly all adults in the US have a cell phone.</a> Reliance on smartphones
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Pew Research shows that nearly all adults in the US have a cell phone (opens in a new tab)">Pew Research shows that nearly all adults in the US have a cell phone.</a> Reliance on smartphones
|
||||
for online access is especially common among Americans with lower household incomes and those with lower levels of
|
||||
formal education. Of those earning less than $30,000 a year, 28% say their mobile phone is the sole method to
|
||||
digitally connect.
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{{notification.key_name}}
|
||||
</span>
|
||||
<span class="grid-col-6 api-notifications-item__meta-time">
|
||||
<time class="timeago" datetime="{{ notification.created_at }}">
|
||||
<time datetime="{{ notification.created_at }}">
|
||||
{{ notification.created_at|format_delta }}
|
||||
</time>
|
||||
</span>
|
||||
|
||||
@@ -20,7 +20,11 @@ Error
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
|
||||
|
||||
<div class="">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-error" class="usa-sr-only">File upload failed</span>
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
|
||||
{% if recipients.too_many_rows %}
|
||||
@@ -150,7 +154,7 @@ Error
|
||||
) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="#content" class="usa-link back-to-top-link">Back to top</a>
|
||||
<a href="#content" class="usa-link back-to-top-link display-block margin-top-2">Back to top</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -21,11 +21,13 @@
|
||||
|
||||
{{ template|string }}
|
||||
<div class="bottom-gutter-3-2">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-success" class="usa-sr-only">File uploaded successfully</span>
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{% if choose_time_form %}
|
||||
{{ choose_time_form.scheduled_for(param_extensions={
|
||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||
'formGroup': {'classes': ''},
|
||||
'attributes': {
|
||||
'data-module': 'radio-select',
|
||||
'data-categories': choose_time_form.scheduled_for.categories|join(','),
|
||||
@@ -37,7 +39,7 @@
|
||||
{% set button_text %}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{{ usaButton({ "text": button_text, "classes": "margin-top-4" }) }}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link_from_preview }) }}
|
||||
{{ usaBackLink(back_link_from_preview) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-failed" class="usa-sr-only">File upload failed</span>
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% if row_errors|length == 1 %}
|
||||
<div class="usa-alert usa-alert--error" role="alert">
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({
|
||||
"href": url_for('main.choose_template', service_id=current_service.id, template_folder_id=template_folder_id) if template_folder_id else url_for('main.choose_template', service_id=current_service.id)
|
||||
"href": url_for('main.choose_template', service_id=current_service.id, template_folder_id=template_folder_id) if template_folder_id else url_for('main.choose_template', service_id=current_service.id),
|
||||
"html": "Back to all templates"
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -14,19 +14,22 @@
|
||||
</h1>
|
||||
|
||||
|
||||
{% call form_wrapper(
|
||||
action=url_for('.find_services_by_name'),
|
||||
class='usa-search margin-bottom-4'
|
||||
) %}
|
||||
{{ form.search(param_extensions={
|
||||
"label": {"text": "Find services by name, partial name, or service ID"}
|
||||
}) }}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{{ usaButton({
|
||||
"text": "Search",
|
||||
"classes": "usa-button search-form__button"
|
||||
}) }}
|
||||
{% endcall %}
|
||||
{% call form_wrapper(
|
||||
action=url_for('.find_services_by_name'),
|
||||
class='usa-search margin-bottom-4'
|
||||
) %}
|
||||
<div class="display-flex flex-justify-start flex-align-end gap-1 width-full">
|
||||
{{ form.search(param_extensions={
|
||||
"label": {"text": "Find services by name, partial name, or service ID"},
|
||||
"classes": "width-full"
|
||||
}) }}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{{ usaButton({
|
||||
"text": "Search",
|
||||
"classes": "usa-button search-form__button"
|
||||
}) }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% call form_wrapper(id='search-form' ) %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -15,17 +15,20 @@
|
||||
|
||||
|
||||
{% call form_wrapper(
|
||||
action=url_for('.find_users_by_email'),
|
||||
class='usa-search margin-bottom-4'
|
||||
action=url_for('.find_users_by_email'),
|
||||
class='usa-search margin-bottom-4'
|
||||
) %}
|
||||
<div class="display-flex flex-align-end gap-1 width-full">
|
||||
{{ form.search(param_extensions={
|
||||
"label": {"text": "Find users by email, or by partial email"}
|
||||
"label": {"text": "Find users by email, or by partial email"},
|
||||
"classes": "width-full"
|
||||
}) }}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{{ usaButton({
|
||||
"text": "Search",
|
||||
"classes": "search-form__button"
|
||||
"classes": "usa-button search-form__button"
|
||||
}) }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% call form_wrapper(id='search-form' ) %}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<p class="hint">This person has never logged in</p>
|
||||
{% else %}
|
||||
<p>Last logged in
|
||||
<time class="timeago" datetime="{{ user.logged_in_at }}">
|
||||
<time datetime="{{ user.logged_in_at }}">
|
||||
{{ user.logged_in_at|format_delta }}
|
||||
</time>
|
||||
</p>
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
<p>
|
||||
When the <a class="use-link usa-link--external"
|
||||
href="https://www.cbpp.org/research/food-assistance/targeted-text-message-outreach-can-increase-wic-enrollment-pilots-show"
|
||||
target="_blank">Center on Budget and Policy Priorities</a> studied WIC, they found key learnings about the
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Center on Budget and Policy Priorities (opens in a new tab)">Center on Budget and Policy Priorities</a> studied WIC, they found key learnings about the
|
||||
quantity of messages delivered, how people engage with messages, and how they take action.
|
||||
</p>
|
||||
|
||||
@@ -68,8 +70,11 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The <a class="use-link usa-link--external" href="https://codeforamerica.org/resources/texting-playbook/"
|
||||
target="_blank">Code for America’s Texting
|
||||
The <a class="use-link usa-link--external"
|
||||
href="https://codeforamerica.org/resources/texting-playbook/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Code for America's Texting Playbook (opens in a new tab)">Code for America’s Texting
|
||||
Playbook</a>
|
||||
reported specific learnings around appointment reminders, completing
|
||||
document submission, and maintenance reminders.
|
||||
|
||||
@@ -142,7 +142,9 @@
|
||||
<p>
|
||||
The Department of Veterans Affairs provides a <a class="use-link usa-link--external"
|
||||
href="https://github.com/department-of-veterans-affairs/va.gov-team/blob/master/products/va-notify/notification-guide.md#vanotify-notification-guide"
|
||||
target="_blank">helpful flow-chart</a> that can help you decide if a text message is needed
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="helpful flow-chart (opens in a new tab)">helpful flow-chart</a> that can help you decide if a text message is needed
|
||||
for the communication problem you are trying to solve.
|
||||
</p>
|
||||
<h2>Use a hypothesis framework to plan your campaign</h2>
|
||||
|
||||
@@ -101,8 +101,11 @@
|
||||
<p class="sms-message-wrapper text-left">
|
||||
{{circle_number(7) }}<b>Dept. of Social Services:</b> Hi {{circle_number(8) }}<b>Julie,</b> Your Medicaid
|
||||
renewal is closing December 31, 2023. You can renew online at {{circle_number(9) }}<a
|
||||
class="use-link usa-link--external" href="https://www.application.yourstate.gov" target="_blank"
|
||||
rel="application.yourstate.gov">https://www.application.yourstate.gov</a> or {{circle_number(10) }}
|
||||
class="use-link usa-link--external"
|
||||
href="https://www.application.yourstate.gov"
|
||||
target="_blank"
|
||||
rel="application.yourstate.gov"
|
||||
aria-label="Yourstate dot gov (opens in a new tab)">https://www.application.yourstate.gov</a> or {{circle_number(10) }}
|
||||
<b>call the number on the back</b> of your Medicaid card.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
{% set links = [
|
||||
{
|
||||
"p_text": 'The <a class="use-link usa-link--external" href="https://www.fcc.gov/sites/default/files/tcpa-rules.pdf"
|
||||
target="_blank">Telephone Consumer
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Telephone Consumer Protection Act (TCPA) (opens in a new tab)"
|
||||
>Telephone Consumer
|
||||
Protection Act (TCPA)</a> (47 USC § 227) is the federal law that impacts how organizations are allowed to
|
||||
communicate
|
||||
in bulk with the public via telephone (including text message or SMS).'
|
||||
@@ -37,7 +40,9 @@
|
||||
{
|
||||
"p_text": 'The <a class="use-link usa-link--external"
|
||||
href="https://www.fcc.gov/document/fcc-provides-best-practices-enable-critical-health-care-coverage-calls"
|
||||
target="_blank">FCC has ruled</a> that <b>Federal</b> and <b>State</b> programs are exempt from the TCPA and can
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="FCC has ruled (opens in a new tab)">FCC has ruled</a> that <b>Federal</b> and <b>State</b> programs are exempt from the TCPA and can
|
||||
send
|
||||
text messages to the public without consent if conducting official business. Without explicit mention in the ruling,
|
||||
local governments, phone carriers, or any texting intermediaries might require it.'
|
||||
@@ -52,7 +57,10 @@
|
||||
case.
|
||||
For additional questions about the enforcement of the TCPA, you can watch <a class="use-link usa-link--external"
|
||||
href="https://www.performance.gov/cx/life-experiences/facing-a-financial-shock/outputs/fcc-training/"
|
||||
target="_blank">a recorded training on public
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="a recorded training on public
|
||||
benefits texting (opens in a new tab)">a recorded training on public
|
||||
benefits texting</a> provided by the FCC.'
|
||||
},
|
||||
] %}
|
||||
@@ -65,7 +73,9 @@
|
||||
<h4 class="usa-collection__heading">
|
||||
<a class="usa-link {% if item.is_downloadable %}usa-link--downloadable{% else %}usa-link--external{% endif %}"
|
||||
href="{% if item.is_downloadable %}{{ asset_url(item.url_link) }}{% else %}{{ item.url_link }}{% endif %}"
|
||||
target="_blank">
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="{{ item.url_text }} (opens in a new tab)">
|
||||
{{ item.url_text }}
|
||||
</a>
|
||||
</h4>
|
||||
@@ -119,6 +129,7 @@
|
||||
process
|
||||
to keep your phone number list up-to-date. The FCC offers a <a class="use-link usa-link--external"
|
||||
href="https://www.fcc.gov/reassigned-numbers-database" target="_blank"
|
||||
aria-label="Reassigned Numbers Database (opens in a new tab)"
|
||||
rel="Reassigned Numbers Database">Reassigned
|
||||
Numbers Database</a> to identify phone numbers that may have been reassigned since your agency obtained them.
|
||||
</p>
|
||||
|
||||
@@ -81,7 +81,10 @@
|
||||
</ul>
|
||||
<h2>What provoking action looks like</h2>
|
||||
<p class="padding-bottom-2"><a class="use-link usa-link--external"
|
||||
href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC10002044/" target="_blank">Evidence
|
||||
href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC10002044/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Evidence shows (opens in a new tab)">Evidence
|
||||
shows</a> that employing
|
||||
behavioral science is an effective way to increase the
|
||||
likelihood of a recipient
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
error_message_with_html=True
|
||||
) }}
|
||||
{% endif %}
|
||||
<p>
|
||||
Team members must be invited directly using their .gov email address, not from forwarded invitations. Invitation links to create a Notify.gov account are active for only 24 hours. Expired invitations can e re-sent from the Team members page.
|
||||
</p>
|
||||
|
||||
{% include 'views/manage-users/permissions.html' %}
|
||||
|
||||
|
||||
@@ -11,10 +11,18 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
{% if not job.finished_processing %}
|
||||
<p class="max-width-full">This page refreshes automatically to show the latest message activity delivery rates, details, and reports.<br>You can watch it in progress or check back later.</p>
|
||||
{% endif %}
|
||||
<div data-job-id="{{ job.id }}" data-feature="{{FEATURE_SOCKET_ENABLED | lower}}" data-host="{{ api_public_url }}">
|
||||
{% if not job.finished_processing %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="status"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="status"
|
||||
data-form=""
|
||||
>
|
||||
@@ -25,8 +33,12 @@
|
||||
{% endif %}
|
||||
{% if not finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="counts"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="counts"
|
||||
data-form=""
|
||||
>
|
||||
@@ -41,8 +53,12 @@
|
||||
</p>
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="notifications"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="notifications"
|
||||
data-form=""
|
||||
>
|
||||
@@ -51,7 +67,6 @@
|
||||
{% if not job.processing_finished %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -30,78 +30,105 @@
|
||||
</div>
|
||||
|
||||
{% if show_search_box %}
|
||||
<div data-module="autofocus">
|
||||
<div>
|
||||
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="user-list">
|
||||
<div class="js-live-search-no-results" style="display: none;">
|
||||
<p class="usa-body margin-0">No results found</p>
|
||||
</div>
|
||||
{% for user in users %}
|
||||
{% if user.status != 'cancelled' %}
|
||||
<div class="user-list-item">
|
||||
<h2 class="user-list-item-heading font-body-lg margin-top-0" title="{{ user.email_address }}">
|
||||
<div class="user-list-item width-full">
|
||||
<h2 class="user-list-item-heading font-body-lg margin-y-0" title="{{ user.email_address }}">
|
||||
{%- if user.name -%}
|
||||
<span class="heading-small live-search-relevant">{{ user.name }}</span>
|
||||
<span class="heading-small live-search-relevant">{{ user.name }}</span>
|
||||
{%- else -%}
|
||||
<span class="heading-small live-search-relevant">{{ user.email_address }}</span>
|
||||
{%- endif -%}
|
||||
</h2>
|
||||
<p class="margin-top-0">
|
||||
{%- if user.status == 'pending' -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(invited)</span>
|
||||
{%- elif user.status == 'cancelled' -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(cancelled invite)</span>
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
<span class="hint">(invited)</span>
|
||||
{%- elif user.status == 'expired' -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(expired invite)</span>
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
<span class="hint">(expired invite)</span>
|
||||
{%- elif user.id == current_user.id -%}
|
||||
<span class="live-search-relevant"></span><span class="hint">(you)</span>
|
||||
{% else %}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
<span class="hint">(you)</span>
|
||||
{%- else -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<h3 class="margin-bottom-0">Permissions</h3>
|
||||
<ul class="tick-cross-list-permissions">
|
||||
{% for permission, label in permissions %}
|
||||
{{ tick_cross(
|
||||
user.has_permission_for_service(current_service.id, permission),
|
||||
label
|
||||
) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{# only show if the service has folders #}
|
||||
{% if current_service.all_template_folders %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
{% set folder_count = user.template_folders_for_service(current_service) | length %}
|
||||
{% if folder_count == 0 %}
|
||||
Cannot see any folders
|
||||
{% elif folder_count != current_service.all_template_folders | length %}
|
||||
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
|
||||
{% else %}
|
||||
Can see all folders
|
||||
{% endif%}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if current_service.has_permission('email_auth') %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
Signs in with
|
||||
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if current_service.has_permission('email_auth') %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
Signs in with
|
||||
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if user.status == 'pending' or user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id)}}">Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
|
||||
</p>
|
||||
|
||||
<div class="usa-accordion usa-accordion--bordered margin-bottom-2">
|
||||
<h3 class="margin-bottom-0 usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button"
|
||||
aria-expanded="false"
|
||||
aria-controls="permissions_{{user.id}}"
|
||||
>
|
||||
Permissions
|
||||
</button>
|
||||
</h3>
|
||||
<div id="permissions_{{user.id}}" class="usa-accordion__content usa-prose" hidden>
|
||||
<ul class="tick-cross-list-permissions">
|
||||
{% for permission, label in permissions %}
|
||||
{{ tick_cross(
|
||||
user.has_permission_for_service(current_service.id, permission),
|
||||
label
|
||||
) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if current_service.all_template_folders %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
{% set folder_count = user.template_folders_for_service(current_service) | length %}
|
||||
{% if folder_count == 0 %}
|
||||
Cannot see any folders
|
||||
{% elif folder_count != current_service.all_template_folders | length %}
|
||||
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
|
||||
{% else %}
|
||||
Can see all folders
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id)}}">Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
|
||||
{% elif user.is_editable_by(current_user) %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id)}}">Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span></a>
|
||||
|
||||
{% if current_service.has_permission('email_auth') %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
Signs in with
|
||||
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if user.status == 'pending' or user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id) }}">
|
||||
Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id) }}">
|
||||
Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span>
|
||||
</a>
|
||||
{% elif user.is_editable_by(current_user) %}
|
||||
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id) }}">
|
||||
Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -55,7 +55,7 @@
|
||||
{% if not error %}
|
||||
{% if choose_time_form %}
|
||||
{{ choose_time_form.scheduled_for(param_extensions={
|
||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||
'formGroup': {'classes': ''},
|
||||
'attributes': {
|
||||
'data-module': 'radio-select',
|
||||
'data-categories': choose_time_form.scheduled_for.categories|join(','),
|
||||
@@ -66,8 +66,8 @@
|
||||
{% set button_text %}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{% endif %}
|
||||
{{ usaButton({ "text": button_text, "classes": "margin-top-2" }) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
{% block backLink %}
|
||||
{% if help %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% else %}
|
||||
{{ usaBackLink({ "href": back_link_from_preview }) }}
|
||||
{{ usaBackLink(back_link_from_preview) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
help='3' if help else 0
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<h3>Does everything look good?</h3>
|
||||
<h3 class="margin-bottom-">Does everything look good?</h3>
|
||||
{% if not error %}
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
@@ -84,9 +84,54 @@
|
||||
{{ usaButton({
|
||||
"text": button_text,
|
||||
"name": button_text
|
||||
}) }}
|
||||
}) }}
|
||||
{{ usaButton({
|
||||
"text": "Cancel",
|
||||
"name": "Cancel",
|
||||
"classes": "usa-button--secondary",
|
||||
"type": "button",
|
||||
"attributes": {
|
||||
"data-open-modal": "cancelModal"
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="usa-modal"
|
||||
data-module="usa-modal"
|
||||
id="cancelModal"
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cancelModalHeading"
|
||||
aria-describedby="cancelModalDesc"
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading font-body-lg" id="cancelModalHeading">Are you sure you want to cancel this message?</h2>
|
||||
<p id="cancelModalDesc">Your template is saved, but your message and recipient details will not be. Canceling will bring you back to the template selection page.</p>
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<a href="{{ url_for('main.choose_template', service_id=current_service.id) }}" class="usa-button">Yes, cancel
|
||||
<span class="usa-sr-only">and return to the template selection page</span></a>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button class="usa-button usa-button--unstyled padding-105 text-center" data-close-modal type="button">
|
||||
No, go back <span class="usa-sr-only">to message send</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="usa-button usa-modal__close" aria-label="Close this window" data-close-modal type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
</h1>
|
||||
|
||||
{% if show_search_box %}
|
||||
<div data-module="autofocus">
|
||||
<div>
|
||||
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="user-list">
|
||||
{% for user in users %}
|
||||
{% for user in users %}
|
||||
<div class="user-list">
|
||||
<div class="user-list-item">
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-9">
|
||||
@@ -60,8 +60,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
{{ usaButton({
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="grid-row bottom-gutter">
|
||||
{% for noti_type in global_stats %}
|
||||
<div class="grid-col-6">
|
||||
<span class="big-number-dark bottom-gutter-2-3">
|
||||
<span class="big-number-dark">
|
||||
<span class="big-number-number">
|
||||
{{ "{:,}".format(noti_type.black_box.number) }}
|
||||
</span>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
<a class="usa-link" target="_blank" href="{{ url_for('main.live_services_csv') }}">Download live services csv report</a>
|
||||
<a class="usa-link" target="_blank" rel="noopener noreferrer" aria-label="Download live services CSV report (opens in a new tab)" href="{{ url_for('main.live_services_csv') }}">Download live services csv report</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="usa-link" href="{{ url_for('main.notifications_sent_by_service') }}">Monthly notification statuses for live services</a>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
@@ -9,7 +10,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -38,10 +39,18 @@
|
||||
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"}) }}
|
||||
{% set is_phone_number = form.placeholder_value.label.text == "phone number" %}
|
||||
{% set label_text = " phone number" if current_placeholder == "phone number" else current_placeholder %}
|
||||
{% set extra_class = "extra-tracking" if is_phone_number else "" %}
|
||||
|
||||
<div class="grid-col-12 {{ extra_class }}">
|
||||
{{ textbox(
|
||||
form.placeholder_value,
|
||||
label=label_text,
|
||||
hint=hint_text,
|
||||
extra_form_group_classes=extra_class,
|
||||
param_extensions={"id": "phone-number", "classes": ""}
|
||||
) }}
|
||||
</div>
|
||||
{% if skip_link or link_to_upload %}
|
||||
<div class="grid-col-12 margin-top-1">
|
||||
|
||||
@@ -7,33 +7,78 @@
|
||||
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
||||
|
||||
{% block service_page_title %}
|
||||
Upload a list of {{ 999|recipient_count_label(template.template_type) }}
|
||||
Upload your bulk-sending spreadsheet
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink(params) }}
|
||||
{{ usaBackLink({
|
||||
"href": url_for('main.send_one_off_step', service_id=current_service.id, template_id=template.id, step_index=0),
|
||||
"html": "Back to select recipients"
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% set phone_numbers = [
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Label column A (the first column) as Phone number",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Double check it's the only column with Phone number as its label",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Make sure no duplicate phone numbers are listed in Column A",
|
||||
}
|
||||
]
|
||||
%}
|
||||
|
||||
{% set additional_data = [
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Match column labels one-to-one to the message template placeholders",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Label each additional <a class=\"usa-link\" href=\"/using-notify/how-to#personalize-content\">personalized placeholder</a> separately",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Separate each word in a column label with a space or dash, but no commas",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Fill in each personalized placeholder with the appropriate data or information",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Fill in each <a class=\"usa-link\" href=\"/using-notify/how-to#conditional-content\">conditional placeholder</a> column with a Yes (Y) or No (N) to “answer” whether the recipient meets its criteria",
|
||||
}
|
||||
]
|
||||
%}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Upload a list of {}'.format(999|recipient_count_label(template.template_type))) }}
|
||||
{{ page_header('Upload your bulk-sending spreadsheet')}}
|
||||
<p class="font-sans-lg text-base">Organize phone numbers and information in a single spreadsheet and upload when you have multiple messages to send. Column headers in the spreadsheet will place the data in the right spots within the template that's selected.</p>
|
||||
|
||||
<div class="page-footer bottom-gutter">
|
||||
<div class="page-footer bottom-gutter margin-top-3">
|
||||
{{file_upload(
|
||||
form.file,
|
||||
allowed_file_extensions=allowed_file_extensions,
|
||||
button_text='Choose a file',
|
||||
button_text='Choose and upload a spreadsheet',
|
||||
show_errors=False
|
||||
)}}
|
||||
</div>
|
||||
<span id="upload-status-live" class="usa-sr-only" role="status" aria-live="polite" aria-atomic="true"></span>
|
||||
|
||||
<h2 class="font-body-lg">Your file needs to look like this example</h2>
|
||||
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
|
||||
|
||||
<div class="spreadsheet" data-module="fullscreen-table">
|
||||
{% call(item, row_number) list_table(
|
||||
example,
|
||||
caption="Example",
|
||||
caption="Example spreadsheet",
|
||||
caption_visible=False,
|
||||
field_headings=[''] + column_headings
|
||||
) %}
|
||||
@@ -44,14 +89,71 @@
|
||||
{% endcall %}
|
||||
</div>
|
||||
<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.
|
||||
Each template saved in Notify is given an example <abbr title="Comma Separated Values">CSV</abbr> formatted spreadsheet like this. It's the most reliable file format for uploading your contact list. Use this example to populate your template with the right data items. Start by downloading this example for your message template. Then save it as a <abbr title="Comma Separated Values">CSV</abbr> file for bulk messaging.
|
||||
</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>
|
||||
<h2 class="font-body-lg">Your bulk-sending spreadsheet checklist</h2>
|
||||
<div class="grid-container margin-top-2 padding-0">
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-12">
|
||||
<h3>Phone numbers</h3>
|
||||
<ul class="usa-icon-list">
|
||||
{% for item in phone_numbers %}
|
||||
<li class="usa-icon-list__item">
|
||||
<div class="usa-icon-list__icon text-green">
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="usa-icon">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}{{ item.svg_src }}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="usa-icon-list__content">
|
||||
{{item.card_heading | safe }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="grid-col-12">
|
||||
<h3>Additional data</h3>
|
||||
<ul class="usa-icon-list">
|
||||
{% for item in additional_data %}
|
||||
<li class="usa-icon-list__item">
|
||||
<div class="usa-icon-list__icon text-green">
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="usa-icon">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}{{ item.svg_src }}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="usa-icon-list__content">
|
||||
{{item.card_heading | safe }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="grid-col-12">
|
||||
<h3>If an error occurs</h3>
|
||||
<ul class="usa-icon-list">
|
||||
<li class="usa-icon-list__item">
|
||||
<div class="usa-icon-list__icon text-red">
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="usa-icon">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}#error"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="usa-icon-list__content">
|
||||
If you're receiving any errors, double check your file format and make sure your data is pasted in with values only
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="font-body-lg margin-bottom-2">You are filling in this template</h2>
|
||||
<p>Template: {{ template.name }}</p>
|
||||
{{ template|string }}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,199 +4,206 @@ import usaButton %} {% block meta %}
|
||||
name="description"
|
||||
content="Notify.gov lets you send text messages to your users. Try it now if you work in federal, state, or local government."
|
||||
/>
|
||||
{% endblock %} {% block pageTitle %} Notify.gov {% endblock %} {% block main %}
|
||||
{% endblock %}
|
||||
{% block pageTitle %} Notify.gov
|
||||
{% if is_api_down %} - There's currently a technical issue {% endif %}
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
{% block beforeContent %}{% endblock %}
|
||||
|
||||
<main id="main-content" role="main">
|
||||
{% block content %}
|
||||
<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="font-sans-2xl usa-hero__heading">
|
||||
Reach people where they are with government-powered text messages
|
||||
</h1>
|
||||
<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
|
||||
{% if is_api_down %}
|
||||
{% include "error/technical_difficulties.html" %}
|
||||
{% else %}
|
||||
<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="font-sans-2xl usa-hero__heading">
|
||||
Reach people where they are with government-powered text messages
|
||||
</h1>
|
||||
<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-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<img
|
||||
src="{{ asset_url('images/logo-login.svg') }}"
|
||||
alt="Login.gov logo"
|
||||
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."
|
||||
/>
|
||||
</a>
|
||||
if you are an existing partner
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<img
|
||||
src="{{ asset_url('images/product/phone-text.png') }}"
|
||||
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
|
||||
Government texting made easy
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Notify.gov is a text messaging platform built for government agencies.
|
||||
With minimal set-up and secure, personalized messaging, you can make
|
||||
one-way texting a part of your outreach program.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<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="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>
|
||||
</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/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>
|
||||
</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/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>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="usa-card-group">
|
||||
<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/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>
|
||||
</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/security.svg') }}"
|
||||
alt="Lock with code icon inside on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Security and privacy</h3>
|
||||
</div>
|
||||
<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>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Send hundreds or thousands of individually customized messages
|
||||
with just a few clicks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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"
|
||||
</section>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<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="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="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>
|
||||
</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/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>
|
||||
</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/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>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="usa-card-group">
|
||||
<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/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>
|
||||
</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/security.svg') }}"
|
||||
alt="Lock with code icon inside on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Security and privacy</h3>
|
||||
</div>
|
||||
<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>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Send hundreds or thousands of individually customized messages
|
||||
with just a few clicks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
22
app/utils/api_health.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_api_down():
|
||||
api_base_url = os.getenv("API_HOST_NAME")
|
||||
try:
|
||||
response = requests.get(api_base_url, timeout=2)
|
||||
is_down = response.status_code != 200
|
||||
if is_down:
|
||||
logger.warning(
|
||||
f"API responded with status {response.status_code} at {api_base_url}"
|
||||
)
|
||||
return is_down
|
||||
except RequestException as e:
|
||||
logger.error(f"API down when loading homepage {e}")
|
||||
return True
|
||||
@@ -198,6 +198,7 @@ def convert_report_date_to_preferred_timezone(db_date_str_in_utc):
|
||||
|
||||
def get_user_preferred_timezone():
|
||||
if current_user and hasattr(current_user, "preferred_timezone"):
|
||||
return current_user.preferred_timezone
|
||||
|
||||
tz = current_user.preferred_timezone
|
||||
if tz in pytz.all_timezones:
|
||||
return tz
|
||||
return "US/Eastern"
|
||||
|
||||
@@ -118,9 +118,10 @@ def indent_njk(s, width=4, first=False, blank=False, indentfirst=None):
|
||||
indention = " " * width
|
||||
newline = "\n"
|
||||
|
||||
# we know 'indention' and 'newline' are safe so use nosec to bypass static scan warning
|
||||
if isinstance(s, Markup):
|
||||
indention = Markup(indention)
|
||||
newline = Markup(newline)
|
||||
indention = Markup(indention) # nosec
|
||||
newline = Markup(newline) # nosec
|
||||
|
||||
s += newline # this quirk is necessary for splitlines method
|
||||
|
||||
@@ -190,7 +191,7 @@ class NunjucksUndefined(jinja2.runtime.Undefined):
|
||||
def items(self):
|
||||
return self
|
||||
|
||||
# Allow escaping with Markup. This is required when
|
||||
# Allow escaping. This is required when
|
||||
# autoescape is enabled. Debugging this issue was
|
||||
# annoying; the error messages were not clear as to
|
||||
# the cause of the issue (see upstream pull request
|
||||
|
||||
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |