mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-25 08:44:23 -04:00
Compare commits
481 Commits
f9a08dffe0
...
06-30-2025
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db20c888f2 | ||
|
|
f12b4e29be | ||
|
|
01b1004ab5 | ||
|
|
8146ff995e | ||
|
|
ff3b9eb820 | ||
|
|
9433d287ff | ||
|
|
a7e1fbefd2 | ||
|
|
5fb66ad2b7 | ||
|
|
492b6f331b | ||
|
|
f97ac4fb18 | ||
|
|
c8c683a74e | ||
|
|
727c6705d2 | ||
|
|
bcbe6d1c61 | ||
|
|
4cc97e8c8c | ||
|
|
2f23ff9d8f | ||
|
|
e33097c808 | ||
|
|
b6e9198986 | ||
|
|
0b28c78e3d | ||
|
|
c94f89d828 | ||
|
|
bde035187d | ||
|
|
fc128d8240 | ||
|
|
2868a86f55 | ||
|
|
68ceda6db5 | ||
|
|
5007523d79 | ||
|
|
18dba4d68c | ||
|
|
3aca70ca4e | ||
|
|
543e8f2f44 | ||
|
|
a16386445a | ||
|
|
2f1ffb443d | ||
|
|
d15f9e583c | ||
|
|
429022f892 | ||
|
|
b1ae92a549 | ||
|
|
b84f2983ce | ||
|
|
de67064c41 | ||
|
|
cdfbc79982 | ||
|
|
9414929b8a | ||
|
|
add3ace98a | ||
|
|
3d0d3c675e | ||
|
|
313b4f499c | ||
|
|
cdce255de0 | ||
|
|
d317240ddf | ||
|
|
80e1540b28 | ||
|
|
310806b736 | ||
|
|
36bf5ff948 | ||
|
|
79f7276769 | ||
|
|
f8100e8db4 | ||
|
|
a109fe1882 | ||
|
|
5b758f79bc | ||
|
|
85dd0eba92 | ||
|
|
f8b634942d | ||
|
|
8f7ba56d6f | ||
|
|
25b955d597 | ||
|
|
83d91ee91a | ||
|
|
c039916312 | ||
|
|
7c964bf448 | ||
|
|
ba02acf837 | ||
|
|
710d942f49 | ||
|
|
6aa866e113 | ||
|
|
8161e68c25 | ||
|
|
f73b44abba | ||
|
|
bc28e3e2f9 | ||
|
|
b31686d6f2 | ||
|
|
c5b8b318fe | ||
|
|
3755922a6a | ||
|
|
6ba59915a6 | ||
|
|
a4d19648b2 | ||
|
|
f9e9c840da | ||
|
|
775a14583d | ||
|
|
063c4b6362 | ||
|
|
3fbbb1b06d | ||
|
|
856a68e7dc | ||
|
|
fa3b013a38 | ||
|
|
ab3584bd9a | ||
|
|
d620243e32 | ||
|
|
17ec285130 | ||
|
|
eaaa7e6b41 | ||
|
|
c2cda68059 | ||
|
|
519f8f7f7d | ||
|
|
f4ecc73b48 | ||
|
|
deae07d301 | ||
|
|
8db4ca47cd | ||
|
|
8b31f73d53 | ||
|
|
1253b9b226 | ||
|
|
3af96cca37 | ||
|
|
1841eb85ad | ||
|
|
ad1174b80c | ||
|
|
38c760088c | ||
|
|
deb8168740 | ||
|
|
e616811c81 | ||
|
|
fe0f8f98ad | ||
|
|
648324b7b9 | ||
|
|
3d8c557ec5 | ||
|
|
11c9c1301f | ||
|
|
982d949e5e | ||
|
|
4abf4068a9 | ||
|
|
f12a977608 | ||
|
|
468216fdf9 | ||
|
|
a57811a8a8 | ||
|
|
0fe16e2ba7 | ||
|
|
740cdcd7a3 | ||
|
|
ae0715eee5 | ||
|
|
29084860d4 | ||
|
|
295e7a23e3 | ||
|
|
286ee10240 | ||
|
|
0f834132b9 | ||
|
|
6495633931 | ||
|
|
a07aa29c7f | ||
|
|
e9eef359d5 | ||
|
|
b97d378231 | ||
|
|
b174e1d675 | ||
|
|
e81f27b3b0 | ||
|
|
e1e62f3567 | ||
|
|
3e9ad2ef17 | ||
|
|
fc9686b202 | ||
|
|
63ec371db1 | ||
|
|
5bdb4cb3b5 | ||
|
|
3354b38b8e | ||
|
|
97c1093de7 | ||
|
|
dec0fb4e63 | ||
|
|
db5c74b861 | ||
|
|
7452a3675c | ||
|
|
6c07780872 | ||
|
|
87d5253bc1 | ||
|
|
518e2b5bce | ||
|
|
b1cbcace38 | ||
|
|
64c9e4c088 | ||
|
|
b84c16a136 | ||
|
|
46f665d0c1 | ||
|
|
2648e10f42 | ||
|
|
7cd6bc2725 | ||
|
|
9293bece83 | ||
|
|
b4b8435fb7 | ||
|
|
672651f3a6 | ||
|
|
888a615d03 | ||
|
|
cf7af35178 | ||
|
|
057d703851 | ||
|
|
5b0c739adb | ||
|
|
6c9e79c7a4 | ||
|
|
d53bc5ff38 | ||
|
|
e8d8bf1eb1 | ||
|
|
c60f2eab0d | ||
|
|
f8f01c99e6 | ||
|
|
bc4a4bcd33 | ||
|
|
14a4169525 | ||
|
|
a8bb555703 | ||
|
|
7ba13c5d0d | ||
|
|
30093673d0 | ||
|
|
81f24f08ef | ||
|
|
f4795843cc | ||
|
|
71d076e32f | ||
|
|
42aca4ebe7 | ||
|
|
9815c9ec9f | ||
|
|
6df7f3f926 | ||
|
|
6970f2f981 | ||
|
|
3156d5e7ae | ||
|
|
312cdf08fa | ||
|
|
fa158cd762 | ||
|
|
1b4c4350ce | ||
|
|
63abd500b6 | ||
|
|
31c58e0be7 | ||
|
|
37fee05bfb | ||
|
|
86f8d66573 | ||
|
|
7b6a6ffe53 | ||
|
|
396459be67 | ||
|
|
a2fab31653 | ||
|
|
1e95628c56 | ||
|
|
3672b16315 | ||
|
|
f290ae30a7 | ||
|
|
e5dc5f12aa | ||
|
|
12b8dd6c4f | ||
|
|
36c7781e37 | ||
|
|
1daccc812c | ||
|
|
19b0e24602 | ||
|
|
71b865cc12 | ||
|
|
526402a7f4 | ||
|
|
a5399ae693 | ||
|
|
e84750fbbf | ||
|
|
6d0bd1cfca | ||
|
|
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 | ||
|
|
5053d5d440 | ||
|
|
55a86bcebc | ||
|
|
91b83ab749 | ||
|
|
9042f58e2c | ||
|
|
cf6c402e66 | ||
|
|
09f1c712bb | ||
|
|
f2b53628df | ||
|
|
f1b31eb85e | ||
|
|
3e5c194a11 | ||
|
|
65d13701c7 | ||
|
|
8722ae8dd9 | ||
|
|
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 | ||
|
|
f661ea39ca | ||
|
|
431dbb10dd | ||
|
|
25524324c5 | ||
|
|
5abc0a4c8e | ||
|
|
b6a6ffb2ab | ||
|
|
b01a62225e | ||
|
|
522340f52b | ||
|
|
fe42f5ef64 | ||
|
|
b4afcb07aa | ||
|
|
5d3364a114 | ||
|
|
70b149476c | ||
|
|
10c61440c9 | ||
|
|
34fdf7a1ca | ||
|
|
20cbb0a8db | ||
|
|
14024a28ca | ||
|
|
280e348fa6 | ||
|
|
77e9475ed8 | ||
|
|
b4e239c9bb | ||
|
|
c016530a7d | ||
|
|
2779e398f6 | ||
|
|
5f258cb699 | ||
|
|
a53df8998c | ||
|
|
e3843f33c6 | ||
|
|
20e0dc6c3e | ||
|
|
d0d6113a05 | ||
|
|
232c6cb9e1 | ||
|
|
ed238bfe6f | ||
|
|
34b39acf38 | ||
|
|
3e0e290dc0 | ||
|
|
d4a2584dfb | ||
|
|
638a4f20da | ||
|
|
da695557b0 | ||
|
|
3e18c8f595 | ||
|
|
f8c7f6b817 | ||
|
|
193184983d | ||
|
|
38451d77ad | ||
|
|
032b9927aa | ||
|
|
06c238269f | ||
|
|
2064fc864a | ||
|
|
f55d47e3d1 | ||
|
|
7813ce7914 | ||
|
|
d79ece2d01 | ||
|
|
606b124912 |
74
.ds.baseline
74
.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": 120,
|
||||
"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",
|
||||
@@ -479,7 +469,7 @@
|
||||
"filename": "tests/app/main/test_errorhandlers.py",
|
||||
"hashed_secret": "005fa73b3f2be8f0d71d361c1f0a9d787cd09b4e",
|
||||
"is_verified": false,
|
||||
"line_number": 33,
|
||||
"line_number": 34,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -537,7 +527,7 @@
|
||||
"filename": "tests/app/main/views/test_register.py",
|
||||
"hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473",
|
||||
"is_verified": false,
|
||||
"line_number": 122,
|
||||
"line_number": 114,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@@ -545,7 +535,7 @@
|
||||
"filename": "tests/app/main/views/test_register.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 199,
|
||||
"line_number": 184,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@@ -553,7 +543,7 @@
|
||||
"filename": "tests/app/main/views/test_register.py",
|
||||
"hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8",
|
||||
"is_verified": false,
|
||||
"line_number": 272,
|
||||
"line_number": 255,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -563,7 +553,7 @@
|
||||
"filename": "tests/app/main/views/test_sign_in.py",
|
||||
"hashed_secret": "8b8b69116ee882b5e987e330f55db81aba0636f9",
|
||||
"is_verified": false,
|
||||
"line_number": 97,
|
||||
"line_number": 95,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -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-04-10T19:38:31Z"
|
||||
"generated_at": "2025-06-18T17:05:12Z"
|
||||
}
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
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
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.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.3"
|
||||
python-version: "3.12.9"
|
||||
- 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
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:
|
||||
|
||||
130
.github/workflows/checks.yml
vendored
130
.github/workflows/checks.yml
vendored
@@ -51,74 +51,73 @@ jobs:
|
||||
- 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
|
||||
|
||||
|
||||
4
.github/workflows/daily_checks.yml
vendored
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
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
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
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
|
||||
|
||||
|
||||
|
||||
52
.github/workflows/drift.yml
vendored
52
.github/workflows/drift.yml
vendored
@@ -24,14 +24,26 @@ jobs:
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
with:
|
||||
path: terraform/staging
|
||||
run: |
|
||||
cd terraform/staging
|
||||
terraform init
|
||||
terraform plan -detailed-exitcode
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "No changes detected. Intrastructure is up-to-date."
|
||||
elif [ $exit_code -eq 2 ]; then
|
||||
echo "Changes detected. Infrastructure drift found."
|
||||
exit 1
|
||||
else
|
||||
echo "Error running terraform plan."
|
||||
exit $exit_code
|
||||
fi
|
||||
|
||||
|
||||
check_demo_drift:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -52,14 +64,25 @@ jobs:
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
with:
|
||||
path: terraform/demo
|
||||
run: |
|
||||
cd terraform/demo
|
||||
terraform init
|
||||
terraform plan -detailed-exitcode
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "No changes detected. Intrastructure is up-to-date."
|
||||
elif [ $exit_code -eq 2 ]; then
|
||||
echo "Changes detected. Infrastructure drift found."
|
||||
exit 1
|
||||
else
|
||||
echo "Error running terraform plan."
|
||||
exit $exit_code
|
||||
fi
|
||||
|
||||
check_prod_drift:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -80,11 +103,22 @@ jobs:
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
with:
|
||||
path: terraform/production
|
||||
run: |
|
||||
cd terraform/production
|
||||
terraform init
|
||||
terraform plan -detailed-exitcode
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "No changes detected. Intrastructure is up-to-date."
|
||||
elif [ $exit_code -eq 2 ]; then
|
||||
echo "Changes detected. Infrastructure drift found."
|
||||
exit 1
|
||||
else
|
||||
echo "Error running terraform plan."
|
||||
exit $exit_code
|
||||
fi
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@
|
||||
*.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
|
||||
|
||||
2
.profile
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"
|
||||
|
||||
14
Makefile
14
Makefile
@@ -17,9 +17,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
|
||||
.PHONY: bootstrap
|
||||
bootstrap: ## Set up everything to run the app
|
||||
make generate-version-file
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
poetry sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
poetry run pre-commit install
|
||||
source $(NVMSH) --no-use && nvm install && npm install
|
||||
@@ -29,9 +27,7 @@ bootstrap: ## Set up everything to run the app
|
||||
.PHONY: bootstrap-with-git-hooks
|
||||
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
|
||||
make generate-version-file
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
poetry sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
git config --global --unset-all core.hooksPath
|
||||
poetry run pre-commit install
|
||||
@@ -106,11 +102,11 @@ py-test: ## Run python unit tests
|
||||
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
|
||||
@@ -131,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:
|
||||
|
||||
24
README.md
24
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.
|
||||
|
||||
@@ -243,12 +243,12 @@ git clone git@github.com:GSA/notifications-admin.git
|
||||
|
||||
Now go into the project directory (`notifications-admin` by default), create a
|
||||
virtual environment, and set the local Python version to point to the virtual
|
||||
environment (assumes version Python `3.12.2` is what is installed on your
|
||||
environment (assumes version Python `3.12.9` is what is installed on your
|
||||
machine):
|
||||
|
||||
```sh
|
||||
cd notifications-admin
|
||||
pyenv virtualenv 3.12.2 notify-admin
|
||||
pyenv virtualenv 3.12.9 notify-admin
|
||||
pyenv local notify-admin
|
||||
```
|
||||
|
||||
@@ -299,7 +299,7 @@ environment with the newer version of Python you just installed:
|
||||
|
||||
```sh
|
||||
cd notifications-admin
|
||||
pyenv virtualenv 3.12.2 notify-admin
|
||||
pyenv virtualenv 3.12.9 notify-admin
|
||||
pyenv local notify-admin
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -24,7 +24,6 @@ from flask_talisman import Talisman
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_wtf.csrf import CSRFError
|
||||
from itsdangerous import BadSignature
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.local import LocalProxy
|
||||
@@ -35,6 +34,7 @@ from app.config import configs
|
||||
from app.extensions import redis_client
|
||||
from app.formatters import (
|
||||
convert_markdown_template,
|
||||
convert_time_unixtimestamp,
|
||||
convert_to_boolean,
|
||||
format_auth_type,
|
||||
format_billions,
|
||||
@@ -109,7 +109,9 @@ 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_python_client.errors import HTTPError
|
||||
from notifications_utils import logging, request_helper
|
||||
from notifications_utils.formatters import (
|
||||
formatted_list,
|
||||
@@ -140,6 +142,9 @@ 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": [
|
||||
@@ -164,8 +169,8 @@ def _csp(config):
|
||||
"'self'",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.google-analytics.com",
|
||||
"http://localhost:6011",
|
||||
"ws://localhost:6011",
|
||||
f"{api_public_url}",
|
||||
f"{api_public_ws_url}",
|
||||
],
|
||||
"style-src": ["'self'", asset_domain],
|
||||
"img-src": ["'self'", asset_domain, logo_domain],
|
||||
@@ -189,6 +194,10 @@ def create_app(application):
|
||||
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():
|
||||
ttl = 24 * 60 * 60
|
||||
@@ -664,6 +673,7 @@ def add_template_filters(application):
|
||||
format_thousands,
|
||||
id_safe,
|
||||
convert_to_boolean,
|
||||
convert_time_unixtimestamp,
|
||||
format_list_items,
|
||||
iteration_count,
|
||||
recipient_count,
|
||||
|
||||
1
app/assets/images/api-error.svg
Normal file
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 |
17
app/assets/images/messaging-devices.svg
Normal file
17
app/assets/images/messaging-devices.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="305" height="304" viewBox="0 0 381 380" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M163.41 47.1289H32.7852C14.983 47.1289 0.5 61.6119 0.5 79.414V293.793C0.755313 301.164 11.3797 301.158 11.6328 293.793V79.414C11.6328 67.7506 21.1217 58.2617 32.7852 58.2617H163.41C175.074 58.2617 184.562 67.7506 184.562 79.414V335.84H11.6328V319.851C11.3775 312.481 0.753086 312.487 0.5 319.851V346.601C0.5 364.403 14.983 378.887 32.7852 378.887H163.41C181.212 378.887 195.695 364.403 195.695 346.601V79.414C195.695 61.6119 181.212 47.1289 163.41 47.1289ZM163.41 367.754H32.7852C21.2464 367.754 11.8414 358.464 11.6425 346.972H184.554C184.354 358.464 174.949 367.754 163.41 367.754Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M72.8629 102.793H123.332C145.461 101.951 145.444 70.2287 123.332 69.3945H72.8629C50.7338 70.2362 50.7509 101.959 72.8629 102.793ZM72.8629 80.5273H123.332C130.701 80.7893 130.696 91.4004 123.332 91.6601H72.8629C65.4937 91.3981 65.4989 80.7871 72.8629 80.5273Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M41.6899 265.26V303.853C41.6899 308.56 45.5189 312.389 50.2251 312.389H145.225C149.931 312.389 153.76 308.56 153.76 303.853V265.26C153.76 260.554 149.931 256.725 145.225 256.725H50.2251C45.5181 256.725 41.6899 260.554 41.6899 265.26ZM52.8228 267.857H142.627V301.256H52.8228V267.857Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M374.934 50.0976C378.008 50.0976 380.5 47.6054 380.5 44.5312V21.4203C380.5 9.60909 370.835 0 358.955 0H258.626C246.746 0 237.081 9.60909 237.081 21.4203V99.174L211.672 116.104C207.446 118.746 207.447 125.434 211.673 128.076L237.081 145.005V195.298C237.081 207.109 246.746 216.718 258.626 216.718H358.955C370.835 216.719 380.5 207.109 380.5 195.298V71.9098C380.245 64.5391 369.62 64.545 369.367 71.9098V195.298C369.367 200.971 364.697 205.586 358.955 205.586H258.626C252.885 205.586 248.214 200.971 248.214 195.298V143.773C248.214 140.812 246.74 138.063 244.272 136.418L222.766 122.09L244.272 107.761C246.741 106.116 248.214 103.367 248.214 100.407V21.4203C248.214 15.7477 252.885 11.1328 258.626 11.1328H358.955C364.697 11.1328 369.367 15.7477 369.367 21.4203V44.5312C369.367 47.6054 371.86 50.0976 374.934 50.0976Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 44.4072H313.172C320.542 44.1519 320.537 33.5275 313.172 33.2744H279.025C271.655 33.5305 271.66 44.1549 279.025 44.4072Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 116.647H313.172C320.542 116.392 320.537 105.768 313.172 105.515H279.025C271.655 105.77 271.66 116.394 279.025 116.647Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 62.4678H336.689C344.059 62.2125 344.054 51.588 336.689 51.335H279.025C271.655 51.5903 271.66 62.2147 279.025 62.4678Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 80.5273H336.689C344.059 80.272 344.054 69.6476 336.689 69.3945H279.025C271.655 69.6498 271.66 80.2743 279.025 80.5273Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M98.0967 205.319C108.123 205.319 116.28 197.161 116.28 187.135C115.366 163.045 80.8237 163.052 79.9131 187.135C79.9131 197.161 88.0705 205.319 98.0967 205.319ZM98.0967 180.084C101.984 180.084 105.147 183.248 105.147 187.135C104.793 196.476 91.3992 196.473 91.0459 187.135C91.0459 183.247 94.2091 180.084 98.0967 180.084Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M49.0186 181.389C49.7927 183.69 51.1932 185.667 53.0569 187.135C51.1932 188.603 49.7935 190.58 49.0186 192.88C47.9662 196.005 48.1948 199.354 49.6621 202.308L56.7158 216.509C59.7425 222.603 67.1644 225.101 73.2592 222.078L77.814 219.82V224.885C77.814 231.693 83.3529 237.233 90.1618 237.233H106.039C112.844 237.233 118.38 231.697 118.38 224.891V219.82L122.935 222.078C129.032 225.101 136.452 222.603 139.479 216.509L146.533 202.305C148 199.352 148.229 196.004 147.177 192.879C146.403 190.578 145.001 188.601 143.138 187.134C147.636 183.694 149.157 177.013 146.532 171.96L139.48 157.763C136.453 151.668 129.029 149.168 122.932 152.192L118.38 154.449V149.378C118.38 142.573 112.844 137.036 106.039 137.036H90.1618C83.3537 137.036 77.814 142.575 77.814 149.384V154.449L73.2622 152.192C67.1651 149.168 59.7417 151.667 56.7144 157.762L49.6621 171.96C48.1948 174.916 47.9662 178.264 49.0186 181.389ZM59.6319 176.914L66.6842 162.717C66.9825 162.115 67.7151 161.867 68.3162 162.167L78.764 167.347C80.9646 168.437 83.5259 168.316 85.6129 167.021C87.7007 165.726 88.9468 163.486 88.9468 161.03V149.385C88.9468 148.715 89.4916 148.17 90.1618 148.17H106.039C106.705 148.17 107.247 148.712 107.247 149.379V161.03C107.247 163.486 108.493 165.726 110.581 167.021C112.668 168.316 115.228 168.439 117.43 167.346L127.878 162.166C128.48 161.868 129.211 162.115 129.51 162.716L136.563 176.913C136.747 177.284 136.698 177.62 136.626 177.836C136.553 178.053 136.388 178.349 136.016 178.533L131.411 180.817C126.23 183.267 126.231 191.002 131.411 193.451L136.019 195.736C136.39 195.92 136.554 196.215 136.626 196.431C136.699 196.647 136.747 196.982 136.563 197.351L129.509 211.555C129.211 212.154 128.48 212.401 127.881 212.103L117.43 206.921C115.229 205.83 112.668 205.952 110.582 207.247C108.494 208.542 107.248 210.782 107.248 213.239V224.89C107.248 225.556 106.706 226.099 106.039 226.099H90.1625C89.4931 226.099 88.9476 225.554 88.9476 224.884V213.239C88.9476 210.782 87.7014 208.542 85.6137 207.247C83.5229 205.95 80.9601 205.832 78.764 206.922L68.3133 212.104C67.7121 212.401 66.9833 212.155 66.6864 211.556L59.6326 197.356C59.4486 196.985 59.4968 196.649 59.5696 196.432C59.6423 196.215 59.807 195.919 60.1781 195.735L64.7842 193.451C69.9654 191.001 69.9639 183.266 64.7842 180.818L60.1789 178.534C59.8078 178.35 59.643 178.053 59.5703 177.837C59.4968 177.62 59.4486 177.285 59.6319 176.914Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M363.426 257.783H235.796C226.382 257.783 218.723 265.442 218.723 274.856V362.927C218.723 372.341 226.383 380 235.796 380H363.426C372.841 380 380.499 372.341 380.499 362.927V274.856C380.499 265.442 372.841 257.783 363.426 257.783ZM369.366 362.927C369.366 366.203 366.702 368.867 363.426 368.867H235.796C232.52 368.867 229.856 366.203 229.856 362.927V274.856C229.856 271.58 232.521 268.916 235.796 268.916H363.426C366.702 268.916 369.366 271.58 369.366 274.856V362.927Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M268.272 141.779V181.381C268.272 185.809 271.875 189.412 276.304 189.412H340.926C345.354 189.412 348.956 185.809 348.956 181.381V141.779C348.956 137.351 345.354 133.749 340.926 133.749H276.304C271.875 133.748 268.272 137.35 268.272 141.779ZM279.405 144.881H337.824V178.279H279.405V144.881Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M273.174 297.591C271.206 295.229 267.695 294.909 265.334 296.878L241.975 316.344C239.33 318.441 239.331 322.8 241.975 324.897L265.334 344.364C266.375 345.23 267.638 345.654 268.895 345.653C270.489 345.653 272.073 344.971 273.174 343.65C275.142 341.289 274.823 337.779 272.462 335.811L254.233 320.62L272.462 305.431C274.823 303.462 275.142 299.953 273.174 297.591Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M357.249 316.344L333.889 296.878C331.527 294.91 328.018 295.229 326.049 297.59C324.081 299.952 324.4 303.462 326.762 305.43L344.99 320.62L326.762 335.81C324.4 337.778 324.081 341.288 326.049 343.65C328.026 346.021 331.541 346.322 333.889 344.362L357.249 324.895C359.893 322.799 359.892 318.441 357.249 316.344Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M314.73 284.336C311.892 283.154 308.634 284.495 307.451 287.333L281.496 349.626C280.314 352.464 281.656 355.723 284.493 356.905C287.26 358.079 290.59 356.775 291.772 353.908L317.727 291.616C318.91 288.778 317.569 285.519 314.73 284.336Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 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);
|
||||
@@ -63,7 +63,7 @@
|
||||
const buttonContent = this.buttonContent[buttonState](this.fieldLabel);
|
||||
const stickyClass = expanded ? ' js-stick-at-bottom-when-scrolling' : '';
|
||||
|
||||
return $(`<div class="selection-footer${stickyClass}">
|
||||
return $(`<div class="selection-footer${stickyClass} margin-top-2">
|
||||
<button
|
||||
class="govuk-button govuk-button--secondary selection-footer__button usa-button usa-button--outline"
|
||||
aria-expanded="${expanded ? 'true' : 'false'}"
|
||||
@@ -78,8 +78,6 @@
|
||||
|
||||
this.module.$formGroup.append(this.$el);
|
||||
|
||||
// make footer sticky if expanded, clear up from it being sticky if not
|
||||
GOVUK.stickAtBottomWhenScrolling.recalculate();
|
||||
};
|
||||
|
||||
function CollapsibleCheckboxes () {}
|
||||
@@ -97,6 +95,7 @@
|
||||
this.total = this.$checkboxes.length;
|
||||
this.legendText = this.$fieldset.find('legend').first().text().trim();
|
||||
this.expanded = false;
|
||||
this.checkUncheckButtonsAdded = false;
|
||||
|
||||
this.addHeadingHideLegend();
|
||||
|
||||
@@ -124,6 +123,44 @@
|
||||
|
||||
this.$fieldset.find('legend').addClass('usa-sr-only');
|
||||
};
|
||||
CollapsibleCheckboxes.prototype.addCheckUncheckAllButtons = function() {
|
||||
const $buttonsContainer = $('<div class="check-uncheck-all-buttons margin-bottom-2"></div>');
|
||||
|
||||
this.$toggleAllButton = $('<button type="button" class="usa-button usa-button--outline usa-button--small">Select all</button>');
|
||||
|
||||
$buttonsContainer.append(this.$toggleAllButton);
|
||||
|
||||
this.summary.$el.after($buttonsContainer);
|
||||
|
||||
this.$toggleAllButton.on('click', this.toggleAll.bind(this));
|
||||
|
||||
this.updateToggleButtonText();
|
||||
};
|
||||
CollapsibleCheckboxes.prototype.toggleAll = function(e) {
|
||||
e.preventDefault();
|
||||
const allChecked = this.$checkboxes.filter(':checked').length === this.$checkboxes.length;
|
||||
|
||||
if (allChecked) {
|
||||
this.$checkboxes.prop('checked', false);
|
||||
} else {
|
||||
this.$checkboxes.prop('checked', true);
|
||||
}
|
||||
|
||||
this.handleSelection();
|
||||
this.updateToggleButtonText();
|
||||
};
|
||||
CollapsibleCheckboxes.prototype.updateToggleButtonText = function() {
|
||||
if (!this.$toggleAllButton) return;
|
||||
|
||||
const checkedCount = this.$checkboxes.filter(':checked').length;
|
||||
const allChecked = checkedCount === this.$checkboxes.length;
|
||||
|
||||
if (allChecked) {
|
||||
this.$toggleAllButton.text('Deselect all');
|
||||
} else {
|
||||
this.$toggleAllButton.text('Select all');
|
||||
}
|
||||
};
|
||||
CollapsibleCheckboxes.prototype.expand = function(e) {
|
||||
if (e !== undefined) { e.preventDefault(); }
|
||||
|
||||
@@ -132,6 +169,15 @@
|
||||
this.expanded = true;
|
||||
this.summary.update(this.getSelection());
|
||||
this.footer.update(this.expanded);
|
||||
|
||||
if (!this.checkUncheckButtonsAdded) {
|
||||
this.addCheckUncheckAllButtons();
|
||||
this.checkUncheckButtonsAdded = true;
|
||||
} else {
|
||||
if (this.$toggleAllButton) {
|
||||
this.$toggleAllButton.parent().show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shift focus whether expanded or not
|
||||
@@ -145,6 +191,10 @@
|
||||
this.expanded = false;
|
||||
this.summary.update(this.getSelection());
|
||||
this.footer.update(this.expanded);
|
||||
|
||||
if (this.$toggleAllButton) {
|
||||
this.$toggleAllButton.parent().hide();
|
||||
}
|
||||
}
|
||||
|
||||
// shift focus whether expanded or not
|
||||
@@ -159,6 +209,7 @@
|
||||
};
|
||||
CollapsibleCheckboxes.prototype.handleSelection = function(e) {
|
||||
this.summary.update(this.getSelection(), this.total, this.fieldLabel);
|
||||
this.updateToggleButtonText();
|
||||
};
|
||||
CollapsibleCheckboxes.prototype.bindEvents = function() {
|
||||
const self = this;
|
||||
|
||||
@@ -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,11 +15,11 @@
|
||||
|
||||
let query = normalize($searchBox.val());
|
||||
let results = 0;
|
||||
let $noResultsMessage = $('.js-live-search-no-results');
|
||||
|
||||
$targets.each(function() {
|
||||
|
||||
let content = $('.live-search-relevant', this).text() || $(this).text();
|
||||
let isMatch = normalize(content).indexOf(normalize(query)) > -1;
|
||||
|
||||
if ($(this).has(':checked').length) {
|
||||
$(this).show();
|
||||
@@ -28,17 +28,27 @@
|
||||
}
|
||||
|
||||
if (query == '') {
|
||||
$(this).css('display', '');
|
||||
$(this).removeClass('js-hidden');
|
||||
results++;
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).toggle(isMatch);
|
||||
|
||||
if (isMatch) { results++; }
|
||||
let isMatch = normalize(content).includes(normalize(query));
|
||||
|
||||
if (isMatch) {
|
||||
$(this).removeClass('js-hidden');
|
||||
results++;
|
||||
} else {
|
||||
$(this).addClass('js-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
if (query !== '' && results === 0) {
|
||||
$noResultsMessage.removeClass('js-hidden');
|
||||
} else {
|
||||
$noResultsMessage.addClass('js-hidden');
|
||||
}
|
||||
|
||||
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
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(`
|
||||
|
||||
@@ -20,6 +20,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
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}` });
|
||||
});
|
||||
|
||||
@@ -197,6 +197,11 @@
|
||||
if (event.currentTarget.value === 'add-new-template' && this.$singleNotificationChannel) {
|
||||
window.location = "/services/" + this.$singleChannelService + "/templates/add-" + this.$singleNotificationChannel;
|
||||
} else {
|
||||
if (this.currentState === 'add-new-template') {
|
||||
this.$form.find('input[type=checkbox]').prop('checked', false);
|
||||
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
|
||||
}
|
||||
|
||||
if (this.stateChanged()) {
|
||||
this.render();
|
||||
}
|
||||
@@ -286,7 +291,29 @@
|
||||
mode = 'dialog';
|
||||
}
|
||||
|
||||
if (currentStateObj && ('setFocus' in currentStateObj)) {
|
||||
if (this.currentState === 'add-new-template') {
|
||||
this.$form.find('.template-list-item').addClass('js-hidden');
|
||||
$('.live-search').addClass('js-hidden');
|
||||
$('#breadcrumb-template-folders').addClass('js-hidden');
|
||||
$('#template-list').addClass('js-hidden');
|
||||
this.$form.find('input[type=checkbox]').prop('checked', false);
|
||||
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
|
||||
|
||||
$('#page-title').text('New Template');
|
||||
$('#page-description').text('Every message starts with a template. Choose to start with a blank template or copy an existing template.');
|
||||
document.title = 'New Templates';
|
||||
} else {
|
||||
this.$form.find('.template-list-item').removeClass('js-hidden');
|
||||
$('.live-search').removeClass('js-hidden');
|
||||
$('#breadcrumb-template-folders').removeClass('js-hidden');
|
||||
$('#template-list').removeClass('js-hidden');
|
||||
|
||||
$('#page-title').text('Select or create a template');
|
||||
$('#page-description').text('Every message starts with a template. To send, choose or create a template.');
|
||||
document.title = 'Select or create a template';
|
||||
}
|
||||
|
||||
if (currentStateObj && 'setFocus' in currentStateObj) {
|
||||
scrollTop = $(window).scrollTop();
|
||||
currentStateObj.setFocus();
|
||||
$(window).scrollTop(scrollTop);
|
||||
|
||||
8
app/assets/js/init.uswds.js
Normal file
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/Studio Research & Development.pdf
Normal file
Binary file not shown.
BIN
app/assets/pdf/Text Campaign Planning.pdf
Normal file
BIN
app/assets/pdf/Text Campaign Planning.pdf
Normal file
Binary file not shown.
BIN
app/assets/pdf/The Notify Story.pdf
Normal file
BIN
app/assets/pdf/The Notify Story.pdf
Normal file
Binary file not shown.
BIN
app/assets/pdf/wa_case_study.pdf
Normal file
BIN
app/assets/pdf/wa_case_study.pdf
Normal file
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -221,6 +229,10 @@ td.table-empty-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.js-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.folder-heading a.folder-heading-folder,
|
||||
.template-list-folder {
|
||||
background: url(../img/material-icons/folder.svg) no-repeat;
|
||||
@@ -289,6 +301,7 @@ td.table-empty-message {
|
||||
margin-top: units(1);
|
||||
width: 100%;
|
||||
border: 1px solid color('gray-60');
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,11 +547,17 @@ td.table-empty-message {
|
||||
background-color: #a1d3ff;
|
||||
}
|
||||
|
||||
#template-list,
|
||||
#template-list > div,
|
||||
.sticky-scroll-area {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#template-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: units(1) 0 units(1) units(1);
|
||||
margin: units(2) 0 units(5);
|
||||
margin: units(2) 0 units(6);
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -862,6 +881,10 @@ $do-dont-top-bar-width: 1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.bg-gray-cool {
|
||||
background-color: #dfe1e2;
|
||||
}
|
||||
|
||||
.svg-circle-icon {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
@@ -1060,3 +1083,15 @@ nav.nav {
|
||||
.usa-site-alert .usa-alert .usa-alert__body {
|
||||
max-width: 75rem;
|
||||
}
|
||||
|
||||
.usa-form-group--nested {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.selection-content > ul > li.usa-checkbox {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.selection-content .usa-form-group--nested li.usa-checkbox {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
@@ -130,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"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from flask import abort, render_template, request, url_for
|
||||
|
||||
from app import current_service, job_api_client
|
||||
from app.formatters import convert_time_unixtimestamp, get_time_left
|
||||
from app.formatters import get_time_left
|
||||
from app.main import main
|
||||
from app.utils.pagination import (
|
||||
generate_next_dict,
|
||||
@@ -78,47 +78,43 @@ def handle_pagination(jobs, service_id, page):
|
||||
return prev_page, next_page, pagination
|
||||
|
||||
|
||||
JOB_STATUS_DELIVERED = "delivered"
|
||||
JOB_STATUS_FAILED = "failed"
|
||||
|
||||
|
||||
def get_job_statistics(job, status):
|
||||
statistics = job.get("statistics", [])
|
||||
for stat in statistics:
|
||||
if stat.get("status") == status:
|
||||
return stat.get("count")
|
||||
return None
|
||||
|
||||
|
||||
def create_job_dict_entry(job):
|
||||
job_id = job.get("id")
|
||||
can_download = get_time_left(job.get("created_at")) != "Data no longer available"
|
||||
activity_time = job.get("processing_started") or job.get("created_at")
|
||||
|
||||
return {
|
||||
"job_id": job_id,
|
||||
"can_download": can_download,
|
||||
"download_link": (
|
||||
url_for(".view_job_csv", service_id=current_service.id, job_id=job_id)
|
||||
if can_download
|
||||
else None
|
||||
),
|
||||
"view_job_link": url_for(
|
||||
".view_job", service_id=current_service.id, job_id=job_id
|
||||
),
|
||||
"activity_time": activity_time,
|
||||
"created_by": job.get("created_by"),
|
||||
"template_name": job.get("template_name"),
|
||||
"delivered_count": get_job_statistics(job, JOB_STATUS_DELIVERED),
|
||||
"failed_count": get_job_statistics(job, JOB_STATUS_FAILED),
|
||||
}
|
||||
|
||||
|
||||
def generate_job_dict(jobs):
|
||||
return [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
"download_link": url_for(
|
||||
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"view_job_link": url_for(
|
||||
".view_job", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"created_at": job["created_at"],
|
||||
"time_sent_data_value": convert_time_unixtimestamp(
|
||||
job["processing_finished"]
|
||||
if job["processing_finished"]
|
||||
else (
|
||||
job["processing_started"]
|
||||
if job["processing_started"]
|
||||
else job["created_at"]
|
||||
)
|
||||
),
|
||||
"processing_finished": job["processing_finished"],
|
||||
"processing_started": job["processing_started"],
|
||||
"created_by": job["created_by"],
|
||||
"template_name": job["template_name"],
|
||||
"delivered_count": next(
|
||||
(
|
||||
stat["count"]
|
||||
for stat in job.get("statistics", [])
|
||||
if stat["status"] == "delivered"
|
||||
),
|
||||
None,
|
||||
),
|
||||
"failed_count": next(
|
||||
(
|
||||
stat["count"]
|
||||
for stat in job.get("statistics", [])
|
||||
if stat["status"] == "failed"
|
||||
),
|
||||
None,
|
||||
),
|
||||
}
|
||||
for job in jobs["data"]
|
||||
]
|
||||
if not jobs or not jobs.get("data"):
|
||||
return []
|
||||
return [create_job_dict_entry(job) for job in jobs["data"]]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from flask import current_app, redirect, render_template, session, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import service_api_client
|
||||
from app.formatters import email_safe
|
||||
from app.main import main
|
||||
from app.main.forms import CreateServiceForm
|
||||
from app.utils.user import user_is_gov_user, user_is_logged_in
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
def _create_service(service_name, organization_type, email_from, form):
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from flask import jsonify, redirect, render_template, session, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import current_service, notification_api_client, service_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import SearchByNameForm
|
||||
from app.models.template_list import TemplateList
|
||||
from app.utils.user import user_has_permissions
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
from notifications_utils.template import SMSPreviewTemplate
|
||||
|
||||
|
||||
@@ -43,10 +43,9 @@ def service_dashboard(service_id):
|
||||
service_data_retention_days = 7
|
||||
|
||||
active_jobs = [job for job in job_response if job["job_status"] != "cancelled"]
|
||||
sorted_jobs = sorted(active_jobs, key=lambda job: job["created_at"], reverse=True)
|
||||
job_lists = [
|
||||
{**job_dict, "finished_processing": job_is_finished(job_dict)}
|
||||
for job_dict in sorted_jobs
|
||||
for job_dict in active_jobs
|
||||
]
|
||||
|
||||
total_messages = service_api_client.get_service_message_ratio(service_id)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from flask import flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import user_api_client
|
||||
from app.event_handlers import create_archive_user_event
|
||||
@@ -8,6 +7,7 @@ from app.main import main
|
||||
from app.main.forms import AdminSearchUsersByEmailForm, AuthTypeForm
|
||||
from app.models.user import User
|
||||
from app.utils.user import user_is_platform_admin
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
@main.route("/find-users-by-email", methods=["GET", "POST"])
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from flask import render_template, request
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import user_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import ForgotPasswordForm
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
@main.route("/forgot-password", methods=["GET", "POST"])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -58,11 +58,11 @@ def view_job(service_id, job_id):
|
||||
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
api_host_name = os.environ.get('API_HOST_NAME')
|
||||
api_public_url = os.environ.get("API_PUBLIC_URL")
|
||||
|
||||
return render_template(
|
||||
"views/jobs/job.html",
|
||||
api_host_name=api_host_name,
|
||||
api_public_url=api_public_url,
|
||||
FEATURE_SOCKET_ENABLED=current_app.config["FEATURE_SOCKET_ENABLED"],
|
||||
job=job,
|
||||
status=request.args.get("status", ""),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from flask import abort, flash, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import current_service, service_api_client
|
||||
from app.event_handlers import (
|
||||
@@ -24,6 +23,7 @@ from app.main.forms import (
|
||||
from app.models.user import InvitedUser, User
|
||||
from app.utils.user import is_gov_user, user_has_permissions
|
||||
from app.utils.user_permissions import permission_options
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users")
|
||||
@@ -49,7 +49,7 @@ def invite_user(service_id, user_id=None):
|
||||
form = form_class(
|
||||
inviter_email_address=current_user.email_address,
|
||||
all_template_folders=current_service.all_template_folders,
|
||||
folder_permissions=[f["id"] for f in current_service.all_template_folders],
|
||||
folder_permissions=[],
|
||||
)
|
||||
|
||||
if user_id:
|
||||
|
||||
@@ -4,7 +4,6 @@ from functools import partial
|
||||
|
||||
from flask import flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import current_organization, org_invite_api_client, organizations_client
|
||||
from app.main import main
|
||||
@@ -27,6 +26,7 @@ from app.models.organization import AllOrganizations, Organization
|
||||
from app.models.user import InvitedOrgUser, User
|
||||
from app.utils.csv import Spreadsheet
|
||||
from app.utils.user import user_has_permissions, user_is_platform_admin
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
@main.route("/organizations", methods=["GET"])
|
||||
|
||||
@@ -15,7 +15,6 @@ from flask import (
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import (
|
||||
billing_api_client,
|
||||
@@ -46,6 +45,7 @@ from app.utils.pagination import (
|
||||
get_page_from_request,
|
||||
)
|
||||
from app.utils.user import user_is_platform_admin
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
COMPLAINT_THRESHOLD = 0.02
|
||||
FAILURE_THRESHOLD = 3
|
||||
|
||||
@@ -13,12 +13,10 @@ from flask import (
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
|
||||
from app import redis_client, user_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
RegisterUserForm,
|
||||
RegisterUserFromOrgInviteForm,
|
||||
SetupUserProfileForm,
|
||||
)
|
||||
@@ -32,15 +30,7 @@ from app.utils.user import is_gov_user
|
||||
@main.route("/register", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def register():
|
||||
if current_user and current_user.is_authenticated:
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
|
||||
form = RegisterUserForm()
|
||||
if form.validate_on_submit():
|
||||
_do_registration(form, send_sms=False)
|
||||
return redirect(url_for("main.registration_continue"))
|
||||
|
||||
return render_template("views/register.html", form=form)
|
||||
abort(404)
|
||||
|
||||
|
||||
@main.route("/register-from-org-invite", methods=["GET", "POST"])
|
||||
@@ -108,10 +98,7 @@ def _do_registration(form, send_sms=True, send_email=True, organization_id=None)
|
||||
|
||||
@main.route("/registration-continue")
|
||||
def registration_continue():
|
||||
if not session.get("user_details"):
|
||||
return redirect(url_for(".show_accounts_or_dashboard"))
|
||||
else:
|
||||
raise Exception("Unexpected routing in registration_continue")
|
||||
abort(404)
|
||||
|
||||
|
||||
def get_invite_data_from_redis(state):
|
||||
|
||||
@@ -17,7 +17,6 @@ from flask import (
|
||||
)
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from xlrd.biffh import XLRDError
|
||||
from xlrd.xldate import XLDateError
|
||||
|
||||
@@ -52,6 +51,7 @@ from app.utils import (
|
||||
from app.utils.csv import Spreadsheet, get_errors_for_csv
|
||||
from app.utils.templates import get_template
|
||||
from app.utils.user import user_has_permissions
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
||||
from notifications_utils.insensitive_dict import InsensitiveDict
|
||||
from notifications_utils.recipients import RecipientCSV, first_column_headings
|
||||
@@ -481,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=(
|
||||
@@ -560,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:
|
||||
@@ -755,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):
|
||||
@@ -879,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
|
||||
|
||||
@@ -12,7 +12,6 @@ from flask import (
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import (
|
||||
billing_api_client,
|
||||
@@ -52,6 +51,7 @@ from app.main.forms import (
|
||||
from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, SENDING_STATUSES
|
||||
from app.utils.time import parse_naive_dt
|
||||
from app.utils.user import user_has_permissions, user_is_platform_admin
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
|
||||
[
|
||||
|
||||
@@ -3,7 +3,6 @@ from functools import partial
|
||||
from flask import abort, flash, jsonify, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import (
|
||||
current_service,
|
||||
@@ -29,6 +28,7 @@ from app.models.template_list import TemplateList, TemplateLists
|
||||
from app.utils import NOTIFICATION_TYPES, should_skip_template_page
|
||||
from app.utils.templates import get_template
|
||||
from app.utils.user import user_has_permissions
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
||||
|
||||
form_objects = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
|
||||
from flask import abort, current_app, request, session
|
||||
from flask_login import AnonymousUserMixin, UserMixin, login_user, logout_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.event_handlers import (
|
||||
@@ -22,6 +21,7 @@ from app.utils.user_permissions import (
|
||||
all_ui_permissions,
|
||||
translate_permissions_from_db_to_ui,
|
||||
)
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
def _get_service_id_from_view_args():
|
||||
|
||||
@@ -2,10 +2,10 @@ import os
|
||||
|
||||
from flask import abort, current_app, has_request_context, request
|
||||
from flask_login import current_user
|
||||
from notifications_python_client import __version__
|
||||
from notifications_python_client.base import BaseAPIClient
|
||||
|
||||
from app.extensions import redis_client
|
||||
from notifications_python_client import __version__
|
||||
from notifications_python_client.base import BaseAPIClient
|
||||
from notifications_utils.clients.redis import RequestCache
|
||||
|
||||
cache = RequestCache(redis_client)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from itertools import chain
|
||||
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app.extensions import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, cache
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
class OrganizationsClient(NotifyAdminAPIClient):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from flask import current_app
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app.notify_client import NotifyAdminAPIClient, cache
|
||||
from app.utils import hilite
|
||||
from app.utils.user_permissions import translate_permissions_from_ui_to_db
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
ALLOWED_ATTRIBUTES = {
|
||||
"name",
|
||||
|
||||
@@ -31,7 +31,7 @@ def get_csv_upload(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 = "\n".join(line for line in data.splitlines() if line.strip())
|
||||
cleaned_data = "\r\n".join(line for line in data.splitlines() if line.strip())
|
||||
filedata["data"] = cleaned_data
|
||||
return filedata
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import time
|
||||
import traceback
|
||||
|
||||
from flask import current_app, jsonify, request
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from redis import RedisError
|
||||
|
||||
from app import status_api_client, version
|
||||
from app.extensions import redis_client
|
||||
from app.status import status
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
|
||||
@status.route("/_status", methods=["GET"])
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
{% 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 %}
|
||||
@@ -29,35 +35,11 @@
|
||||
|
||||
{% block header %}
|
||||
{% include 'components/usa_banner.html' %}
|
||||
{% if current_user.is_authenticated or current_service or current_user.platform_admin %}
|
||||
<section class="usa-site-alert usa-site-alert--info" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
GSA will no longer offer the Notify.gov service after June 8th, 2025. Visit
|
||||
<a class="usa-link" href="/notify-service-ending">Notify.gov Service Ending</a> for more information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-heading" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
Notify.gov is no longer accepting new partners.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% include 'components/header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-container minh-mobile">
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -132,7 +114,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>
|
||||
@@ -149,7 +131,7 @@
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button type="button" class="usa-button usa-button--unstyled padding-105 text-center" id="logOutTimer"
|
||||
<button type="button" class="text-center usa-button usa-button--unstyled padding-105" id="logOutTimer"
|
||||
data-close-modal>
|
||||
Sign out
|
||||
</button>
|
||||
@@ -168,7 +150,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 usa-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>
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
{% set is_about_page = request.path.startswith('/about') %}
|
||||
{% set is_contact_page = request.path.startswith('/contact') %}
|
||||
{% set is_information_section = is_about_page or is_contact_page %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": request.path.startswith('/accounts')},
|
||||
{"href": url_for('main.get_started'), "text": "Using Notify", "active": request.path.startswith('/using-notify')},
|
||||
{"href": url_for('main.support'), "text": "Contact us", "active": header_navigation.is_selected('support')}
|
||||
] %}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": request.path.startswith('/accounts')},
|
||||
{"href": url_for('main.get_started'), "text": "Using Notify", "active": request.path.startswith('/using-notify')},
|
||||
{"href": url_for('main.support'), "text": "Contact us", "active": header_navigation.is_selected('support')}
|
||||
] %}
|
||||
|
||||
{% if current_user.platform_admin %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin')}] %}
|
||||
{% else %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile')}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_service %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active": secondary_navigation.is_selected('settings')},
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.platform_admin %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin')}] %}
|
||||
{% else %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile')}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_service %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active": secondary_navigation.is_selected('settings')},
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% set navigation = [
|
||||
{"href": url_for('main.about_notify'), "text": "About Notify", "active": is_about_page},
|
||||
{"href": url_for('main.contact'), "text": "Contact us", "active": is_contact_page}
|
||||
] %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
@@ -59,16 +43,7 @@
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not current_user.is_authenticated and is_information_section%}
|
||||
<div class="usa-nav__login">
|
||||
<a class="usa-button usa-button login-button login-button--primary margin-right-2"
|
||||
href="{{ initial_signin_url }}">Sign
|
||||
in with <img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
@@ -103,7 +78,6 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -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
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>
|
||||
@@ -70,7 +70,7 @@
|
||||
<span>Template</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Time sent</span>
|
||||
<span>Started</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Sender</span>
|
||||
@@ -96,13 +96,12 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-field template">{{ job.template_name }}</td>
|
||||
<td data-sort-value="{{job.time_sent_data_value}}" class="table-field time-sent">
|
||||
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||
<td data-sort-value="{{ job.activity_time | convert_time_unixtimestamp }}" class="table-field time-sent">
|
||||
{{ job.activity_time|format_datetime_table }}
|
||||
</td>
|
||||
<td class="table-field sender">{{ job.created_by.name }}</td>
|
||||
<td class="table-field report">
|
||||
{% if job.time_left != "Data no longer available" %}
|
||||
{% if job.can_download %}
|
||||
<a href="{{ job.download_link }}">
|
||||
<img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="">
|
||||
<span class="usa-sr-only">Download report link</span>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
<div data-job-id="{{ job.id }}" data-feature="{{FEATURE_SOCKET_ENABLED | lower}}" data-host="{{ api_host_name }}">
|
||||
{% 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-resource="{{ updates_url }}"
|
||||
|
||||
@@ -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>
|
||||
<div class="js-live-search-no-results" style="display: none;">
|
||||
<p class="usa-body margin-0">No results found</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="user-list">
|
||||
{% 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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
|
||||
|
||||
{% 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 = [
|
||||
@@ -68,13 +71,14 @@
|
||||
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">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
|
||||
) %}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
{% call row() %}
|
||||
{{ text_field('Send text messages') }}
|
||||
{{ boolean_field('sms' in current_service.permissions) }}
|
||||
{{ edit_field(
|
||||
{# {{ edit_field(
|
||||
'Change',
|
||||
url_for(
|
||||
'.service_set_channel',
|
||||
@@ -48,7 +48,7 @@
|
||||
),
|
||||
permissions=['manage_service'],
|
||||
suffix='your settings for sending text messages',
|
||||
)}}
|
||||
)}} #}
|
||||
{% endcall %}
|
||||
|
||||
{% if current_user.platform_admin %}
|
||||
|
||||
@@ -4,199 +4,72 @@ 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
|
||||
<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/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>
|
||||
{% if is_api_down %}
|
||||
{% include "error/technical_difficulties.html" %}
|
||||
{% else %}
|
||||
<section class="usa-section--dark usa-hero usa-prose" aria-label="Introduction">
|
||||
<div class="grid-container">
|
||||
<div class="grid-row">
|
||||
<div class="text-center grid-col-12 padding-y-4">
|
||||
<h1 class="font-sans-2xl usa-hero__heading margin-bottom-2">
|
||||
Sunsetting Notify.gov
|
||||
</h1>
|
||||
<p class="font-sans-lg margin-0">
|
||||
This beta product is no longer in service as of June 9, 2025.
|
||||
</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
|
||||
</div>
|
||||
</section>
|
||||
<section class="usa-section usa-prose" 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">
|
||||
<h2 class="font-body-xl">
|
||||
Text messaging services
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Though the Notify.gov service is no longer available, government programs can continue to explore text messaging services.
|
||||
</p>
|
||||
<p class="usa-body">Commercial messaging providers may support government use cases. For agencies with the resources to set up and support it, <a href="https://github.com/GSA/notifications-api" class="usa-link">the notifications service codebase</a> that runs Notify.gov is open source and can be reused.</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">
|
||||
<div
|
||||
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<img
|
||||
src="{{ asset_url('images/contact.svg') }}"
|
||||
alt=""
|
||||
class="height-7 width-7"
|
||||
src="{{ asset_url('images/messaging-devices.svg') }}"
|
||||
alt="Illustration showing multiple devices including mobile phones and messaging interfaces representing government text messaging communication"
|
||||
width="305"
|
||||
height="304"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
<p class="margin-0">
|
||||
<strong>Contact us</strong><br />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="bg-gray-cool usa-section usa-prose" aria-label="Thank you message to partners">
|
||||
<div class="grid-container">
|
||||
<div class="grid-row flex-justify-center">
|
||||
<div class="text-center grid-col-8">
|
||||
<h2 class="font-body-xl text-primary margin-bottom-2">
|
||||
To our partners
|
||||
</h2>
|
||||
<p class="usa-body margin-0">
|
||||
Thank you to our users and partners for their support, participation, and contributions in developing Notify.gov.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<nav id="template-list" aria-label="Choose reply">
|
||||
<ul>
|
||||
{% for item in templates_and_folders %}
|
||||
<li class="template-list-item {% if item.ancestors %}template-list-item-hidden-by-default{% endif %} {% if not item.ancestors %}template-list-item-without-ancestors{% endif %}">
|
||||
<li class="template-list-item {% if item.ancestors %}template-list-item-hidden-by-default{% else %}template-list-item-without-ancestors{% endif %}">
|
||||
{% for ancestor in item.ancestors %}
|
||||
<a href="{{ url_for('.conversation_reply', service_id=current_service.id, notification_id=notification_id, from_folder=ancestor.id) }}" class="usa-link template-list-folder">
|
||||
{{ ancestor.name }}
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
{% else %}
|
||||
|
||||
<div class="grid-row flex-column">
|
||||
<h1 class="font-body-2xl line-height-sans-2 margin-bottom-1 margin-top-0">{{page_title}}</h1>
|
||||
<h1 class="font-body-2xl line-height-sans-2 margin-bottom-1 margin-top-0" id="page-title">{{page_title}}</h1>
|
||||
<div class="{% if current_user.has_permissions('manage_templates') %} grid-col-10 {% else %} grid-col-12 {% endif %}">
|
||||
<p class="margin-top-0 margin-bottom-4">
|
||||
<p class="margin-top-0 margin-bottom-4" id="page-description">
|
||||
Every message starts with a template. To send, choose or create a template.
|
||||
</p>
|
||||
{{ folder_path(
|
||||
@@ -59,6 +59,10 @@
|
||||
|
||||
{{ live_search(target_selector='#template-list .template-list-item', show=show_search_box, form=search_form) }}
|
||||
|
||||
<div class="js-live-search-no-results js-hidden">
|
||||
<p class="usa-body margin-top-2">No templates found</p>
|
||||
</div>
|
||||
|
||||
{% if current_user.has_permissions('manage_templates') and user_has_template_folder_permission %}
|
||||
{% call form_wrapper(
|
||||
class='sticky-scroll-area',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% from "components/folder-path.html" import copy_folder_path, page_title_folder_path %}
|
||||
{% from "components/live-search.html" import live_search %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% extends "withnav_template.html" %}
|
||||
{% set page_title = "Copy an existing template" %}
|
||||
@@ -8,6 +9,12 @@
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{% if not from_service %}
|
||||
{{ usaBackLink({ "href": url_for('.choose_template', service_id=current_service.id),
|
||||
"html": "Back to all templates" }) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="">
|
||||
@@ -25,11 +32,14 @@
|
||||
form=search_form,
|
||||
autofocus=True
|
||||
) }}
|
||||
<div class="js-live-search-no-results" style="display: none;">
|
||||
<p class="usa-body margin-top-2">No templates found</p>
|
||||
</div>
|
||||
<nav id="template-list" aria-label="Copy template list">
|
||||
<ul>
|
||||
{% for item in services_templates_and_folders %}
|
||||
|
||||
<li class="template-list-item margin-bottom-2 {% if item.ancestors %}template-list-item-hidden-by-default{% endif %} {% if not item.ancestors %}template-list-item-without-ancestors{% endif %}">
|
||||
<li class="template-list-item margin-bottom-2">
|
||||
{% if item.is_service %}
|
||||
{% elif item.is_folder %}
|
||||
{% else %}
|
||||
|
||||
@@ -56,35 +56,33 @@
|
||||
{{ template._template.updated_at|format_delta }}
|
||||
</time>
|
||||
</span>
|
||||
<div>
|
||||
<a class="usa-link margin-right-3"
|
||||
href="{{ url_for('.view_template_versions', service_id=current_service.id, template_id=template.id) }}">See previous versions</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_templates') and user_has_template_permission %}
|
||||
<h2 class="margin-bottom-1 font-body-lg">More actions</h2>
|
||||
<ul class="usa-list usa-list--unstyled">
|
||||
<li>
|
||||
<a class="usa-link margin-right-3"
|
||||
href="{{ url_for('.view_template_versions', service_id=current_service.id, template_id=template.id) }}">
|
||||
See previous versions
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.has_permissions('manage_templates') and user_has_template_permission %}
|
||||
{% if not template._template.archived %}
|
||||
<li>
|
||||
<a class="usa-link margin-right-3"
|
||||
href="{{ url_for('.delete_service_template', service_id=current_service.id, template_id=template.id) }}">
|
||||
Delete this template
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if not template._template.redact_personalisation %}
|
||||
<li>
|
||||
<a class="usa-link"
|
||||
href="{{ url_for('.confirm_redact_template', service_id=current_service.id, template_id=template.id) }}">
|
||||
Hide all personalized and conditional content after sending
|
||||
</a> for increased privacy protection
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<p class="hint">Personalization is hidden after sending</p>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if not template._template.archived %}
|
||||
<li>
|
||||
<a class="usa-link margin-right-3"
|
||||
href="{{ url_for('.delete_service_template', service_id=current_service.id, template_id=template.id) }}">
|
||||
Delete this template
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if not template._template.redact_personalisation %}
|
||||
<li>
|
||||
<a class="usa-link"
|
||||
href="{{ url_for('.confirm_redact_template', service_id=current_service.id, template_id=template.id) }}">
|
||||
Hide all personalized and conditional content after sending
|
||||
</a> for increased privacy protection
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<p class="hint">Personalization is hidden after sending</p>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
22
app/utils/api_health.py
Normal file
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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user