Compare commits
568 Commits
08-15-2024
...
save_csv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3590c87334 | ||
|
|
8c53c137f6 | ||
|
|
92674bfeea | ||
|
|
01e52b9a20 | ||
|
|
044f1cf170 | ||
|
|
8f89d25909 | ||
|
|
3afffde6cb | ||
|
|
312612a04d | ||
|
|
fe00050e1f | ||
|
|
822fa6f397 | ||
|
|
e9b515d4cc | ||
|
|
923a345d19 | ||
|
|
6e3a569a0a | ||
|
|
14bbabcab7 | ||
|
|
95a39cfd31 | ||
|
|
f363a8e184 | ||
|
|
d798d3695e | ||
|
|
c8ed66cbfe | ||
|
|
b532ce8959 | ||
|
|
fc09e70579 | ||
|
|
a090c90ed6 | ||
|
|
a39c844c30 | ||
|
|
a02448a0fa | ||
|
|
0500da354d | ||
|
|
ff194b2f2b | ||
|
|
0aa2c76f9a | ||
|
|
2503eac887 | ||
|
|
1564507e13 | ||
|
|
737d7b70bf | ||
|
|
937e258d2b | ||
|
|
569de1ab49 | ||
|
|
154912fda5 | ||
|
|
8c3b83091d | ||
|
|
78474aea65 | ||
|
|
643b4a3d7c | ||
|
|
66d18ced10 | ||
|
|
334cd4041a | ||
|
|
4d0d92f825 | ||
|
|
60e9ac18f5 | ||
|
|
1933dad137 | ||
|
|
278541fa5b | ||
|
|
452ab7eb86 | ||
|
|
005e97f034 | ||
|
|
4700534155 | ||
|
|
02e3efd105 | ||
|
|
4de7b5a5ec | ||
|
|
77716e254e | ||
|
|
fd68a918e7 | ||
|
|
64b979c0f7 | ||
|
|
b806fd5eec | ||
|
|
7ed04c82d6 | ||
|
|
a601906ef5 | ||
|
|
b352a13328 | ||
|
|
d7db335a99 | ||
|
|
94f90454e3 | ||
|
|
de02780bd8 | ||
|
|
70c95cdeb6 | ||
|
|
a11bd0e344 | ||
|
|
822682f256 | ||
|
|
6a6a54e889 | ||
|
|
bdb3e777c5 | ||
|
|
cf3c7be9bf | ||
|
|
d8ae3f3056 | ||
|
|
96a8ccc64a | ||
|
|
ccdf9f16d8 | ||
|
|
b4f4e19665 | ||
|
|
1f1f8bcd75 | ||
|
|
e4445b097e | ||
|
|
09f400ad8e | ||
|
|
01f1175e52 | ||
|
|
12b2d0d86d | ||
|
|
2bbc58c8af | ||
|
|
4b85ae6493 | ||
|
|
3010ae1d9e | ||
|
|
cf4ac930c5 | ||
|
|
09896b98cd | ||
|
|
14880bf7ee | ||
|
|
846b5b51e7 | ||
|
|
00ad7b7581 | ||
|
|
e64ba86a2b | ||
|
|
4dd8b578a9 | ||
|
|
a9948131cc | ||
|
|
543239d078 | ||
|
|
e6c0f045a5 | ||
|
|
1b18b4c146 | ||
|
|
661d9b5517 | ||
|
|
a349fb82dc | ||
|
|
a715b33fd0 | ||
|
|
9ce00acec3 | ||
|
|
353e2bb9a6 | ||
|
|
c0d05c99a2 | ||
|
|
c95e949856 | ||
|
|
ff4380b5a0 | ||
|
|
f2ea113856 | ||
|
|
f4b6ca4a79 | ||
|
|
341e5d4cb2 | ||
|
|
4f802587b9 | ||
|
|
3013d27d7c | ||
|
|
17e12495dd | ||
|
|
35d95e4f5c | ||
|
|
e04adba4fd | ||
|
|
866a4ad7bd | ||
|
|
579a65c92d | ||
|
|
7275dac6a0 | ||
|
|
8c40538334 | ||
|
|
da6882774b | ||
|
|
d0cda995cf | ||
|
|
82cef7dbd3 | ||
|
|
e2020c188a | ||
|
|
7db678ddec | ||
|
|
7775508c97 | ||
|
|
3601b16c7b | ||
|
|
eaed2fc879 | ||
|
|
00f63aed23 | ||
|
|
185f00cd01 | ||
|
|
088c2244a3 | ||
|
|
93bc3792be | ||
|
|
417fac6dd1 | ||
|
|
4b62d0e685 | ||
|
|
149065576f | ||
|
|
4eed138dc6 | ||
|
|
7e505f7fda | ||
|
|
4973cb84b1 | ||
|
|
c2ea4684f6 | ||
|
|
ca60eb03c0 | ||
|
|
a3c021b154 | ||
|
|
31076781a3 | ||
|
|
e54d18170e | ||
|
|
c3f4482677 | ||
|
|
0183f37c7b | ||
|
|
fc6adc6771 | ||
|
|
1dbb369143 | ||
|
|
f32845752a | ||
|
|
ae0f8d58d9 | ||
|
|
90e8017cd5 | ||
|
|
34ccfb5bd7 | ||
|
|
2504e58a87 | ||
|
|
db5626f5b0 | ||
|
|
e62cc021e9 | ||
|
|
2624bd885d | ||
|
|
a5ff9af1eb | ||
|
|
8e29229d8b | ||
|
|
c30dbec6ce | ||
|
|
bfead7ffac | ||
|
|
29854c6164 | ||
|
|
978031744f | ||
|
|
f27703112a | ||
|
|
035c4158f9 | ||
|
|
c47144a8b0 | ||
|
|
54d5ee96d0 | ||
|
|
fd447e9a75 | ||
|
|
9ee0da0b32 | ||
|
|
2fbce24f97 | ||
|
|
10da94dbf8 | ||
|
|
d86b1afb9a | ||
|
|
34f133cf62 | ||
|
|
10a8d1b938 | ||
|
|
b7b23ba6c8 | ||
|
|
a65f9f9bbf | ||
|
|
1cc760c153 | ||
|
|
175a47310e | ||
|
|
77f96e901b | ||
|
|
338fbfc407 | ||
|
|
5adcfae5d8 | ||
|
|
b8b4a707a3 | ||
|
|
61ea8e0b03 | ||
|
|
edca721f5b | ||
|
|
3ad6f9daae | ||
|
|
947f85ba51 | ||
|
|
65c99bf8e8 | ||
|
|
d8feea6bd5 | ||
|
|
98fb68ad14 | ||
|
|
6213a9bc96 | ||
|
|
9c59e40de7 | ||
|
|
c89a6089a0 | ||
|
|
971486be20 | ||
|
|
cd6c9af0bc | ||
|
|
7eb7ed1855 | ||
|
|
ea93b57190 | ||
|
|
8df380ca0d | ||
|
|
3865c5b952 | ||
|
|
f37b89849e | ||
|
|
397bbeabde | ||
|
|
a11c567de3 | ||
|
|
bdc27c10d0 | ||
|
|
f7f449ff07 | ||
|
|
b133def7fc | ||
|
|
b4b15bc209 | ||
|
|
44524eb9e2 | ||
|
|
4b8b078476 | ||
|
|
22f9ff603a | ||
|
|
3f7e600ac1 | ||
|
|
5a5b0649e0 | ||
|
|
a23fe5c046 | ||
|
|
306391c958 | ||
|
|
e41ad9fab9 | ||
|
|
fb83e8610c | ||
|
|
d0a435becd | ||
|
|
62f58c6e27 | ||
|
|
0db68453b9 | ||
|
|
fbba1a735f | ||
|
|
35eb2a9013 | ||
|
|
108e93d06a | ||
|
|
4d4288d3c7 | ||
|
|
c0ffa63f88 | ||
|
|
89413c56ba | ||
|
|
e6a3c0d4b7 | ||
|
|
7d2f7d1c8a | ||
|
|
f118b1c4bf | ||
|
|
85a5e3075a | ||
|
|
59959cfab4 | ||
|
|
68cf422ca8 | ||
|
|
a9999aa8ec | ||
|
|
3a50fff6ea | ||
|
|
c15d388fc2 | ||
|
|
ec3ebe5b90 | ||
|
|
fbdc01b109 | ||
|
|
60e3c5291e | ||
|
|
c178a3e77e | ||
|
|
4e3eb03040 | ||
|
|
29b9e122ba | ||
|
|
63c1652c40 | ||
|
|
f28a618e78 | ||
|
|
aa7c7b7d9a | ||
|
|
4c0221e295 | ||
|
|
21d1b69c22 | ||
|
|
5aac3130b3 | ||
|
|
75ce12f5da | ||
|
|
21bd20f607 | ||
|
|
371eb58392 | ||
|
|
a27b9a701b | ||
|
|
c328044636 | ||
|
|
77f44d4d5e | ||
|
|
73b3a61746 | ||
|
|
c5b2535b8e | ||
|
|
9449b80975 | ||
|
|
cc3d70303c | ||
|
|
ef528360be | ||
|
|
ae1a51ff79 | ||
|
|
e9b364010a | ||
|
|
a8bf2414b4 | ||
|
|
ced229167f | ||
|
|
8deed320f8 | ||
|
|
114814b0b5 | ||
|
|
88262302e6 | ||
|
|
2e8f82be3b | ||
|
|
d63cfb3cb0 | ||
|
|
6980e28786 | ||
|
|
c9acf937cf | ||
|
|
37bb89402e | ||
|
|
f20803ecae | ||
|
|
addf4f0959 | ||
|
|
b7f26454ff | ||
|
|
a669477f48 | ||
|
|
8c932a6072 | ||
|
|
d2c654214b | ||
|
|
3120f82b1e | ||
|
|
0e45588e5a | ||
|
|
d4223429ec | ||
|
|
1ea15d5ba1 | ||
|
|
915b18bbbd | ||
|
|
cecfeb7441 | ||
|
|
f2fc6da0ee | ||
|
|
e96aec9898 | ||
|
|
e11b6376c2 | ||
|
|
4e2630d675 | ||
|
|
0a5ca7d0d6 | ||
|
|
64a01e70b7 | ||
|
|
4eff686e17 | ||
|
|
282e27034d | ||
|
|
466137ba07 | ||
|
|
0d04524525 | ||
|
|
08c97a8066 | ||
|
|
6815493f4a | ||
|
|
f578a52010 | ||
|
|
40a0418731 | ||
|
|
b7d699feae | ||
|
|
8b9293db0e | ||
|
|
6ff8be5d3e | ||
|
|
475bd0c1b4 | ||
|
|
2c7d090b82 | ||
|
|
4f193e362c | ||
|
|
687372fd6c | ||
|
|
302a79b41f | ||
|
|
f0299c4042 | ||
|
|
bc5b980431 | ||
|
|
08384dd1fa | ||
|
|
bdc5614fff | ||
|
|
060db76640 | ||
|
|
b2b758ca8d | ||
|
|
584ccce53d | ||
|
|
dda788b073 | ||
|
|
feff8ec1be | ||
|
|
cfc9acc6f2 | ||
|
|
959a680664 | ||
|
|
a5e72ea4eb | ||
|
|
122343e46e | ||
|
|
86b217b1df | ||
|
|
982f77413e | ||
|
|
3967612eb1 | ||
|
|
b0f0a8d8db | ||
|
|
fd2de9bf9b | ||
|
|
cd24c2b029 | ||
|
|
63747515db | ||
|
|
55f588f13d | ||
|
|
81a629935c | ||
|
|
49ec3dc559 | ||
|
|
647b37ae61 | ||
|
|
7b2d495c1b | ||
|
|
47f0952125 | ||
|
|
e0fb031cf5 | ||
|
|
72eff3235e | ||
|
|
67ad9148d6 | ||
|
|
2f40be8cd0 | ||
|
|
7d1c7fabc2 | ||
|
|
a088409dfe | ||
|
|
ddd413f299 | ||
|
|
67097957f5 | ||
|
|
250a36836d | ||
|
|
f8fd0422bf | ||
|
|
4cdac9eba3 | ||
|
|
76863cd4dc | ||
|
|
3d00b0c94f | ||
|
|
4594dc317f | ||
|
|
5e93f2d572 | ||
|
|
a2dbb6c9e6 | ||
|
|
d097a0e2a6 | ||
|
|
cff1fb8f2e | ||
|
|
5a1b59561f | ||
|
|
1aad5f0ac1 | ||
|
|
d89fa8b987 | ||
|
|
d3855f47ac | ||
|
|
6f91d60030 | ||
|
|
9eb54bd837 | ||
|
|
968f7881e7 | ||
|
|
f4d8816eac | ||
|
|
99ce42fc4a | ||
|
|
899347d349 | ||
|
|
14b37524ae | ||
|
|
5caa26e93c | ||
|
|
5ee559da10 | ||
|
|
73852a54f2 | ||
|
|
ec806ce385 | ||
|
|
8eaaf1b067 | ||
|
|
296ebb5655 | ||
|
|
d9805495de | ||
|
|
8c3cdabb92 | ||
|
|
4e52d60a7c | ||
|
|
a5889d3448 | ||
|
|
5234706ae1 | ||
|
|
70c9801f1d | ||
|
|
df22530fa4 | ||
|
|
df630b58d8 | ||
|
|
f9337b614f | ||
|
|
09a775fdad | ||
|
|
62cfa2c39c | ||
|
|
8b6f719510 | ||
|
|
b953d316c4 | ||
|
|
5c3b5790ae | ||
|
|
d0d3878881 | ||
|
|
b6e0249adb | ||
|
|
4a8d768b6f | ||
|
|
876db282d5 | ||
|
|
028423adc1 | ||
|
|
56f7ad47e5 | ||
|
|
1368384fdf | ||
|
|
17c63c0790 | ||
|
|
d21e85278e | ||
|
|
a4b9c16763 | ||
|
|
c44084f325 | ||
|
|
cf39b32808 | ||
|
|
84f3c61bc0 | ||
|
|
892705b125 | ||
|
|
ef9e3aecbe | ||
|
|
6da52f91b3 | ||
|
|
5eadcc0319 | ||
|
|
8d6478c3c1 | ||
|
|
a267cf3d4b | ||
|
|
f82bcb0509 | ||
|
|
c3a2441ade | ||
|
|
8ef357ea61 | ||
|
|
286166c2ad | ||
|
|
7b9e5288f8 | ||
|
|
df85f66a43 | ||
|
|
a91d6d25c8 | ||
|
|
88c47d15f6 | ||
|
|
6537eb2b1d | ||
|
|
75727520c0 | ||
|
|
d6c8ee90e8 | ||
|
|
a3ef868924 | ||
|
|
bdf4c9597b | ||
|
|
6e3db9d59a | ||
|
|
fc88ca55e7 | ||
|
|
b8edfa8175 | ||
|
|
1a247260ff | ||
|
|
92229707e2 | ||
|
|
5db3d4878f | ||
|
|
b1f1005441 | ||
|
|
6223341826 | ||
|
|
6b3fde3356 | ||
|
|
749e4cc708 | ||
|
|
a2d542c726 | ||
|
|
b67b15b6c9 | ||
|
|
d94a35cd30 | ||
|
|
26e53e76c5 | ||
|
|
6cf540be04 | ||
|
|
e3007f273e | ||
|
|
46a980c505 | ||
|
|
b32dde1a07 | ||
|
|
87581705c6 | ||
|
|
3aa68a3d25 | ||
|
|
1308894191 | ||
|
|
35dcfccd5f | ||
|
|
ddf9c6d3ec | ||
|
|
9e9ba9186a | ||
|
|
b5e2bf705d | ||
|
|
5ada9914b4 | ||
|
|
c075cacae9 | ||
|
|
da7d618460 | ||
|
|
9340069454 | ||
|
|
6621144061 | ||
|
|
f6a1f1af5b | ||
|
|
8b81c5ce7d | ||
|
|
ce74175be1 | ||
|
|
f74bf271a5 | ||
|
|
e8b12de581 | ||
|
|
dcc8055377 | ||
|
|
ab96955982 | ||
|
|
c36561ee4d | ||
|
|
c51a5593a7 | ||
|
|
be14a4a0c4 | ||
|
|
65ef22f26e | ||
|
|
e704cb1b78 | ||
|
|
2cae3462d3 | ||
|
|
5beb9fcce1 | ||
|
|
7fea566201 | ||
|
|
3158b91378 | ||
|
|
6f2c7be4ce | ||
|
|
b15b3a67f6 | ||
|
|
8cc14ec4bf | ||
|
|
4dbd9ede92 | ||
|
|
38fde60348 | ||
|
|
b1ad888b65 | ||
|
|
43bafbb617 | ||
|
|
c5060d3f14 | ||
|
|
15a87ec026 | ||
|
|
2690a059d2 | ||
|
|
5cf80ec5dc | ||
|
|
6fffa3cf43 | ||
|
|
e4c99dc15b | ||
|
|
33e616a8f1 | ||
|
|
d4b9a95b29 | ||
|
|
ed1ef5cd4a | ||
|
|
caefc958df | ||
|
|
981281f725 | ||
|
|
4cacd108b1 | ||
|
|
68c0763dec | ||
|
|
3f7d1ffffe | ||
|
|
7e38e28a5e | ||
|
|
f5baf1cb79 | ||
|
|
67f128c26e | ||
|
|
88af5de89d | ||
|
|
7d1a50a5b0 | ||
|
|
758efa8215 | ||
|
|
e9a8e2769a | ||
|
|
f27d53116e | ||
|
|
b20c8ec909 | ||
|
|
664d07b414 | ||
|
|
da6e9f4e8c | ||
|
|
549855c957 | ||
|
|
20428f7485 | ||
|
|
d00f4b5f46 | ||
|
|
d756b2289f | ||
|
|
8f15a83415 | ||
|
|
bb106c04f1 | ||
|
|
5a4402fd8b | ||
|
|
3d808cd3e5 | ||
|
|
87d4911f89 | ||
|
|
407f049360 | ||
|
|
e15c89703b | ||
|
|
9907409efd | ||
|
|
7b6b363532 | ||
|
|
f78fe350b0 | ||
|
|
0c612581cd | ||
|
|
ca7c2e0c93 | ||
|
|
1768a4bbd9 | ||
|
|
3456bb47a1 | ||
|
|
8e3cd6324f | ||
|
|
c67407f090 | ||
|
|
50e67d2308 | ||
|
|
85bafde78d | ||
|
|
c7b83596ba | ||
|
|
77d49754c6 | ||
|
|
1361c2f836 | ||
|
|
5b14a4ea90 | ||
|
|
769e63ebc7 | ||
|
|
b40cb5a9ad | ||
|
|
6ae00cabfe | ||
|
|
13cdb10d88 | ||
|
|
8c0fd48e06 | ||
|
|
4f9f546f23 | ||
|
|
4ca7087218 | ||
|
|
75f6ee6837 | ||
|
|
25722deb68 | ||
|
|
27c6098cc8 | ||
|
|
f553b863c6 | ||
|
|
5243878ef7 | ||
|
|
3b1bd05a10 | ||
|
|
10b65efe1f | ||
|
|
91f79f694e | ||
|
|
1bfd619057 | ||
|
|
03fae4d08f | ||
|
|
67b928085c | ||
|
|
cd98439ac3 | ||
|
|
f0ec9e3da9 | ||
|
|
66a555a2d4 | ||
|
|
339797558a | ||
|
|
672fed7e4b | ||
|
|
5a3f3e5ca0 | ||
|
|
6b236bfa0f | ||
|
|
0c1fc478da | ||
|
|
01eb194a08 | ||
|
|
2c7d9d48dc | ||
|
|
1d03d73642 | ||
|
|
b61329f86d | ||
|
|
062eefcf7a | ||
|
|
8b2a3c26fa | ||
|
|
b7a0dc2225 | ||
|
|
6dc9efd13d | ||
|
|
1eb54f8252 | ||
|
|
eca30e5b20 | ||
|
|
0b75486210 | ||
|
|
5adc45d591 | ||
|
|
4cdcdc075c | ||
|
|
932963beb5 | ||
|
|
136e82102b | ||
|
|
b84d00bfb1 | ||
|
|
0ea4d8897e | ||
|
|
8a3bdd181d | ||
|
|
a81dc83133 | ||
|
|
cc24604aa4 | ||
|
|
ea8cfbdadb | ||
|
|
d1ed7e1d0c | ||
|
|
f9169fcbcb | ||
|
|
17b21b2382 | ||
|
|
170527cfdf | ||
|
|
7b570bfbed | ||
|
|
bc72288d4d | ||
|
|
e50b4bb5f7 | ||
|
|
32f1bb4f87 | ||
|
|
594ad74006 | ||
|
|
3b9664f220 | ||
|
|
9897385837 | ||
|
|
4ec3bc5892 | ||
|
|
358971164e | ||
|
|
282611ec9c | ||
|
|
4a6539197f | ||
|
|
e92fd011ca | ||
|
|
9e0fe69d2b | ||
|
|
0ce3d5033c | ||
|
|
ff110de97f | ||
|
|
d54c339994 | ||
|
|
3dc73ab92e | ||
|
|
42570416da | ||
|
|
e934e49137 | ||
|
|
cec1e4cff0 | ||
|
|
31762239dd | ||
|
|
017198754f |
20
.ds.baseline
@@ -133,15 +133,7 @@
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 65,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Basic Auth Credentials",
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 99,
|
||||
"line_number": 68,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -169,7 +161,7 @@
|
||||
"filename": "app/config.py",
|
||||
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
|
||||
"is_verified": false,
|
||||
"line_number": 117,
|
||||
"line_number": 121,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -433,7 +425,7 @@
|
||||
"filename": "deploy-config/sandbox.yml",
|
||||
"hashed_secret": "113151dd10316fcb0d5507b6215d78e2f3fe9e54",
|
||||
"is_verified": false,
|
||||
"line_number": 8,
|
||||
"line_number": 9,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -601,7 +593,7 @@
|
||||
"filename": "tests/app/main/views/test_user_profile.py",
|
||||
"hashed_secret": "8072d7aad32964ec43fbcb699c75dc38890792f7",
|
||||
"is_verified": false,
|
||||
"line_number": 350,
|
||||
"line_number": 336,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@@ -609,7 +601,7 @@
|
||||
"filename": "tests/app/main/views/test_user_profile.py",
|
||||
"hashed_secret": "4c9dbb972da179e4f66f023eaa5fb9451d835030",
|
||||
"is_verified": false,
|
||||
"line_number": 351,
|
||||
"line_number": 337,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -692,5 +684,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2024-08-15T16:29:15Z"
|
||||
"generated_at": "2024-10-29T19:28:03Z"
|
||||
}
|
||||
|
||||
16
.github/actions/deploy-proxy/action.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Deploy egress proxy
|
||||
description: Set egress space security groups and deploy proxy
|
||||
inputs:
|
||||
cf_org:
|
||||
description: The org the target app exists in.
|
||||
required: true
|
||||
cf_space:
|
||||
description: The space the target app exists in.
|
||||
required: true
|
||||
@@ -16,6 +19,19 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install cf-cli
|
||||
shell: bash
|
||||
run: |
|
||||
curl -A "cg-deploy-action" -v -L -o cf-cli_amd64.deb 'https://packages.cloudfoundry.org/stable?release=debian64&version=v8&source=github'
|
||||
sudo dpkg -i cf-cli_amd64.deb
|
||||
- name: Login to cf-cli
|
||||
shell: bash
|
||||
run: |
|
||||
cf api api.fr.cloud.gov
|
||||
cf auth
|
||||
- name: Target org and space
|
||||
shell: bash
|
||||
run: cf target -o ${{ inputs.cf_org }} -s ${{ inputs.cf_space }}
|
||||
- name: Set restricted space egress
|
||||
shell: bash
|
||||
run: ./terraform/set_space_egress.sh -t -s ${{ inputs.cf_space }}
|
||||
|
||||
8
.github/dependabot.yml
vendored
@@ -9,8 +9,16 @@ updates:
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "A-Shumway42"
|
||||
reviewers:
|
||||
- "A-Shumway42"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
versioning-strategy: increase
|
||||
assignees:
|
||||
- "alexjanousekGSA"
|
||||
reviewers:
|
||||
- "alexjanousekGSA"
|
||||
|
||||
70
.github/workflows/checks.yml
vendored
@@ -53,11 +53,13 @@ jobs:
|
||||
|
||||
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
|
||||
@@ -89,65 +91,33 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- name: Clone API
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: GSA/notifications-api
|
||||
path: "notifications-api"
|
||||
- name: Install API dependencies
|
||||
working-directory: "notifications-api"
|
||||
run: make bootstrap
|
||||
env:
|
||||
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
|
||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||
REDIS_URL: redis://localhost:6379
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_ENVIRONMENT: development
|
||||
- name: Run API server
|
||||
working-directory: "notifications-api"
|
||||
run: make run-procfile &
|
||||
env:
|
||||
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
|
||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||
REDIS_URL: redis://localhost:6379
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_ENVIRONMENT: development
|
||||
- name: Check API Server availability
|
||||
run: |
|
||||
curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
- name: Run Admin server
|
||||
run: make run-flask &
|
||||
# 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
|
||||
|
||||
env:
|
||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov
|
||||
API_HOST_NAME: http://localhost:6011
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
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
|
||||
- name: Run E2E tests
|
||||
# Run the E2E tests against the code found in this PR.
|
||||
# run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
|
||||
# --browser webkit doesn't work at this time.
|
||||
run: make e2e-test
|
||||
# Debugging for now to troubleshoot a connectivity issue to the local servers
|
||||
# run: curl --request GET --url "http://localhost:6012"
|
||||
env:
|
||||
API_HOST_NAME: http://localhost:6011
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
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
|
||||
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
|
||||
17
.github/workflows/deploy-demo.yml
vendored
@@ -47,11 +47,12 @@ jobs:
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: 18f/cg-deploy-action@main
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: "notify-admin"
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
@@ -63,21 +64,22 @@ jobs:
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
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"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-demo
|
||||
push_arguments: >-
|
||||
cf_command: >-
|
||||
push -f manifest.yml
|
||||
--vars-file deploy-config/demo.yml
|
||||
--var DANGEROUS_SALT="$DANGEROUS_SALT"
|
||||
--var SECRET_KEY="$SECRET_KEY"
|
||||
--var ADMIN_CLIENT_USERNAME="notify-admin"
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
@@ -85,6 +87,9 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--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"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
@@ -97,6 +102,10 @@ jobs:
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
CF_PASSWORD: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
with:
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-demo
|
||||
app: notify-admin-demo
|
||||
|
||||
17
.github/workflows/deploy-prod.yml
vendored
@@ -47,11 +47,12 @@ jobs:
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: 18f/cg-deploy-action@main
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: "notify-admin"
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
@@ -63,21 +64,22 @@ jobs:
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
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"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-production
|
||||
push_arguments: >-
|
||||
cf_command: >-
|
||||
push -f manifest.yml
|
||||
--vars-file deploy-config/production.yml
|
||||
--var DANGEROUS_SALT="$DANGEROUS_SALT"
|
||||
--var SECRET_KEY="$SECRET_KEY"
|
||||
--var ADMIN_CLIENT_USERNAME="notify-admin"
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
@@ -85,6 +87,9 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--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"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
@@ -97,6 +102,10 @@ jobs:
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
CF_PASSWORD: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
with:
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-production
|
||||
app: notify-admin-production
|
||||
|
||||
19
.github/workflows/deploy.yml
vendored
@@ -53,11 +53,12 @@ jobs:
|
||||
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: 18f/cg-deploy-action@main
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: "notify-admin"
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
@@ -68,22 +69,23 @@ jobs:
|
||||
LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
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=STATEE"
|
||||
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"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-staging
|
||||
push_arguments: >-
|
||||
cf_command: >-
|
||||
push -f manifest.yml
|
||||
--vars-file deploy-config/staging.yml
|
||||
--var DANGEROUS_SALT="$DANGEROUS_SALT"
|
||||
--var SECRET_KEY="$SECRET_KEY"
|
||||
--var ADMIN_CLIENT_USERNAME="notify-admin"
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
@@ -91,6 +93,9 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--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"
|
||||
--strategy rolling
|
||||
|
||||
|
||||
- name: Check for changes to egress config
|
||||
@@ -104,7 +109,11 @@ jobs:
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
CF_PASSWORD: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
with:
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-staging
|
||||
app: notify-admin-staging
|
||||
|
||||
|
||||
6
.gitignore
vendored
@@ -14,6 +14,7 @@
|
||||
*.XLSX
|
||||
|
||||
## Non user files allowed to be commited
|
||||
!app/assets/pdf/tcpa_overview.pdf
|
||||
!tests/test_pdf_files/no_eof_marker.pdf
|
||||
!tests/test_pdf_files/multi_page_pdf.pdf
|
||||
!tests/test_pdf_files/big.pdf
|
||||
@@ -134,3 +135,8 @@ playwright/
|
||||
|
||||
# Nodenv
|
||||
.node-version
|
||||
|
||||
# BackstopJS
|
||||
backstop_data/bitmaps_test/
|
||||
backstop_data/html_report/
|
||||
backstop_data/engine_scripts/playwright/
|
||||
|
||||
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
2
Makefile
@@ -88,7 +88,7 @@ dead-code: ## 60% is our aspirational goal, but currently breaks the build
|
||||
.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
|
||||
poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
|
||||
DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
|
||||
|
||||
.PHONY: js-lint
|
||||
js-lint: ## Run javascript linting scanners
|
||||
|
||||
10
README.md
@@ -507,3 +507,13 @@ insurance.
|
||||
For more information on what we're working on, the Notify tool, and how to get
|
||||
involved with our team,
|
||||
[see our flyer.](https://github.com/GSA/notifications-admin/blob/main/docs/notify-pilot-flyer.md)
|
||||
|
||||
## Updating secrets for the E2E tests
|
||||
|
||||
At some point, E2E tests will fail because the secrets held in VCAP_SERVICES have expired. To refresh
|
||||
them, you will need to do the following:
|
||||
|
||||
1. Log in the normal way to access cloudfoundry command line options
|
||||
2. In your terminal, run `chmod +x print_vcap.sh`
|
||||
3. In your terminal, run `./print_vcap.sh`
|
||||
4. Copy the value in your terminal and paste it into the VCAP_SERVICES secret in Github on the staging tier.
|
||||
|
||||
2
app/.well-known/security.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Contact: mailto:notify-support@gsa.gov
|
||||
Expires: 2035-10-15T23:59:59Z
|
||||
@@ -17,7 +17,7 @@
|
||||
href="/static/images/apple-touch-icon.png?a0f7e1b728a42016b247dc54ee40d055">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="/static/images/site.webmanifest">
|
||||
<link rel="manifest" href="{{ asset_url('images/site.webmanifest') }}">
|
||||
|
||||
<link rel="stylesheet" media="screen" href="/static/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
|
||||
<link rel="stylesheet" media="print" href="/static/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
|
||||
@@ -68,11 +68,8 @@
|
||||
<p class="usa-body">
|
||||
Try again later.
|
||||
</p>
|
||||
<p class="usa-body">
|
||||
You can check our <a class="usa-link"
|
||||
href="https://status.notifications.service.gov.uk/">system status</a> page to see if there are any known
|
||||
issues.<br />To report a problem, email <a
|
||||
href="mailto:gov-uk-notify-support@digital.cabinet-office.gov.uk">gov-uk-notify-support@digital.cabinet-office.gov.uk</a>
|
||||
<p>To report a problem you can email us at <a class="usa-link" href="mailto:notify-support@gsa.gov">notify-support@gsa.gov</a>.</p>
|
||||
<p>You can expect a response within one business day.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
119
app/assets/images/best-practices-card-sprite.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
1
app/assets/images/call-center.svg
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
1
app/assets/images/community-org.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
1
app/assets/images/grouping.svg
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
3
app/assets/images/phone_grey.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 563 670" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M553.831 334.751V335.974L555.03 335.731C555.104 335.716 555.181 335.708 555.262 335.708H557.846C558.483 335.708 559 336.224 559 336.862V418.708C559 419.345 558.483 419.862 557.846 419.862H555.262C555.181 419.862 555.104 419.853 555.03 419.838L553.831 419.595V420.818V955.446C553.831 970.12 547.528 991.015 535.312 1008.3C522.978 1025.76 503.794 1040.6 477.723 1040.6H82.2769C56.206 1040.6 37.022 1025.76 24.6885 1008.3C12.472 991.015 6.16923 970.12 6.16923 955.446V420.818V419.595L4.97026 419.838C4.89621 419.853 4.8188 419.862 4.73846 419.862H2.15385C1.51659 419.862 1 419.345 1 418.708V358.4C1 357.763 1.51659 357.246 2.15385 357.246H4.73846C4.8188 357.246 4.89621 357.254 4.97026 357.269L6.16923 357.513V356.289V347.588V346.364L4.97026 346.608C4.89621 346.623 4.8188 346.631 4.73846 346.631H2.15385C1.51659 346.631 1 346.114 1 345.477V285.169C1 284.532 1.51659 284.015 2.15385 284.015H4.73846C4.8188 284.015 4.89621 284.023 4.97026 284.038L6.16923 284.282V283.058V261.434V260.21L4.97026 260.454C4.89621 260.469 4.8188 260.477 4.73846 260.477H2.15385C1.51659 260.477 1 259.96 1 259.323V220.554C1 219.917 1.51659 219.4 2.15385 219.4H4.73846C4.8188 219.4 4.89624 219.408 4.97036 219.423L6.16923 219.666V218.443V86.1538C6.16923 71.4804 12.472 50.5848 24.6885 33.2974C37.022 15.8443 56.206 1 82.2769 1H477.723C503.794 1 522.978 15.8443 535.312 33.2974C547.528 50.5848 553.831 71.4804 553.831 86.1538V334.751ZM400.615 16.2308H399.615V17.2308V43.0769C399.615 49.6619 394.277 55 387.692 55H172.308C165.723 55 160.385 49.6619 160.385 43.0769V17.2308V16.2308H159.385H82.2769C62.6883 16.2308 47.6257 27.2304 37.1269 42.0872C26.5125 57.1076 21.4 74.9813 21.4 86.1538V955.446C21.4 966.619 26.5125 984.492 37.1269 999.513C47.6257 1014.37 62.6883 1025.37 82.2769 1025.37H477.723C497.312 1025.37 512.374 1014.37 522.873 999.513C533.487 984.492 538.6 966.619 538.6 955.446V86.1538C538.6 74.9813 533.488 57.1076 522.873 42.0872C512.374 27.2304 497.312 16.2308 477.723 16.2308H400.615Z" stroke="black" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
app/assets/images/project-management.svg
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
app/assets/images/snap-renewal.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
1
app/assets/images/website.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="m291.532 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m254.455 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m217.378 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m123.925 51.273-76.763-.057c-.002 0-.003 0-.005 0-4.14 0-7.497 3.354-7.5 7.494-.003 4.143 3.353 7.503 7.495 7.506l76.763.057h.005c4.14 0 7.497-3.354 7.5-7.494.002-4.142-3.353-7.503-7.495-7.506z"/><path d="m287.507 124.863h-111.062c-6.933 0-12.574 5.641-12.574 12.574v33.479c0 6.948 5.641 12.602 12.574 12.602h111.062c6.933 0 12.574-5.653 12.574-12.602v-33.479c0-6.933-5.641-12.574-12.574-12.574zm-2.426 43.655h-106.209v-28.654h106.209z"/><path d="m292.581 210.217h-121.209c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h121.209c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m189.882 245.736h-18.51c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h18.51c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m292.581 245.736h-62.05c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h62.05c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m444.802 450.744h-.255c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h.255c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m490.667 301.75h-59.114v-129.049c0-11.669-9.494-21.163-21.163-21.163h-71.7v-114.592c0-11.404-9.278-20.682-20.682-20.682h-297.327c-11.404.001-20.681 9.278-20.681 20.682v241.718c0 11.419 9.277 20.709 20.681 20.709h33.801v97.131c0 11.669 9.494 21.163 21.163 21.163h104.223l-8.866 29.646h-14.994c-13.342 0-24.196 10.867-24.196 24.226v11.707c0 6.887 5.603 12.489 12.489 12.489h197.434c6.887 0 12.489-5.603 12.489-12.489v-11.707c0-13.358-10.867-24.226-24.225-24.226h-14.991l-8.847-29.646h71.443v56.735c0 11.763 9.557 21.333 21.305 21.333h91.758c11.763 0 21.333-9.57 21.333-21.333v-151.346c0-11.748-9.57-21.306-21.333-21.306zm-475.667-23.086v-177.438h137.039c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5h-137.039v-49.28c0-3.133 2.548-5.682 5.681-5.682h297.327c3.133 0 5.682 2.549 5.682 5.682v49.279h-137.011c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h137.011v177.438c0 3.147-2.549 5.709-5.682 5.709h-297.327c-3.133.001-5.681-2.56-5.681-5.708zm303.008 20.709c11.404 0 20.682-9.29 20.682-20.709v-112.126h71.7c3.398 0 6.163 2.765 6.163 6.163v129.049h-17.644c-11.748 0-21.305 9.558-21.305 21.306v41.683h-308.122v-65.365h248.526zm21.215 172.166v9.196h-192.411v-9.196c0-5.087 4.125-9.226 9.196-9.226h173.991c5.086 0 9.224 4.139 9.224 9.226zm-79.014-68.872c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h30.298l8.847 29.646h-112.696l8.866-29.646h30.188c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5h-150.067c-3.398 0-6.163-2.765-6.163-6.163v-16.766h308.123v22.929zm236.791 71.735c0 3.492-2.841 6.333-6.333 6.333h-91.758c-3.477 0-6.305-2.841-6.305-6.333v-151.346c0-3.477 2.828-6.306 6.305-6.306h91.758c3.492 0 6.333 2.829 6.333 6.306z"/><path d="m127.661 124.863h-76.451c-6.949 0-12.603 5.641-12.603 12.574v110.696c0 6.949 5.653 12.603 12.603 12.603h76.451c6.949 0 12.602-5.653 12.602-12.603v-110.695c0-6.934-5.653-12.575-12.602-12.575zm-2.398 120.873h-71.655v-105.873h71.655z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -33,30 +33,43 @@
|
||||
.attr('id', 'tooltip')
|
||||
.style('display', 'none');
|
||||
}
|
||||
// Create legend
|
||||
|
||||
// Calculate total messages
|
||||
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData);
|
||||
|
||||
// Create legend only if there are messages
|
||||
const legendContainer = d3.select('.chart-legend');
|
||||
legendContainer.selectAll('*').remove(); // Clear any existing legend
|
||||
|
||||
const legendData = [
|
||||
{ label: 'Delivered', color: COLORS.delivered },
|
||||
{ label: 'Failed', color: COLORS.failed }
|
||||
];
|
||||
if (totalMessages > 0) {
|
||||
// Show legend if there are messages
|
||||
const legendData = [
|
||||
{ label: 'Delivered', color: COLORS.delivered },
|
||||
{ label: 'Failed', color: COLORS.failed }
|
||||
];
|
||||
|
||||
const legendItem = legendContainer.selectAll('.legend-item')
|
||||
.data(legendData)
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'legend-item');
|
||||
const legendItem = legendContainer.selectAll('.legend-item')
|
||||
.data(legendData)
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'legend-item');
|
||||
|
||||
legendItem.append('div')
|
||||
.attr('class', 'legend-rect')
|
||||
.style('background-color', d => d.color)
|
||||
.style('display', 'inline-block')
|
||||
.style('margin-right', '5px');
|
||||
legendItem.append('div')
|
||||
.attr('class', 'legend-rect')
|
||||
.style('background-color', d => d.color)
|
||||
.style('display', 'inline-block')
|
||||
.style('margin-right', '5px');
|
||||
|
||||
legendItem.append('span')
|
||||
.attr('class', 'legend-label')
|
||||
.text(d => d.label);
|
||||
legendItem.append('span')
|
||||
.attr('class', 'legend-label')
|
||||
.text(d => d.label);
|
||||
|
||||
// Ensure the legend is shown
|
||||
legendContainer.style('display', 'flex');
|
||||
} else {
|
||||
// Hide the legend if there are no messages
|
||||
legendContainer.style('display', 'none');
|
||||
}
|
||||
|
||||
const x = d3.scaleBand()
|
||||
.domain(labels)
|
||||
@@ -64,7 +77,7 @@
|
||||
.padding(0.1);
|
||||
// Adjust the y-axis domain to add some space above the tallest bar
|
||||
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0)));
|
||||
const y = d3.scaleLinear()
|
||||
const y = d3.scaleSqrt()
|
||||
.domain([0, maxY + 2]) // Add 2 units of space at the top
|
||||
.nice()
|
||||
.range([height, 0]);
|
||||
@@ -201,6 +214,8 @@
|
||||
deliveredData = [];
|
||||
failedData = [];
|
||||
|
||||
let totalMessages = 0;
|
||||
|
||||
for (var dateString in data) {
|
||||
if (data.hasOwnProperty(dateString)) {
|
||||
const dateParts = dateString.split('-');
|
||||
@@ -209,11 +224,38 @@
|
||||
labels.push(formattedDate);
|
||||
deliveredData.push(data[dateString].sms.delivered);
|
||||
failedData.push(data[dateString].sms.failure);
|
||||
|
||||
// Calculate the total number of messages
|
||||
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
|
||||
}
|
||||
}
|
||||
|
||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||
// Check if there are no messages sent
|
||||
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
|
||||
if (totalMessages === 0) {
|
||||
// Remove existing chart and render the alert message
|
||||
d3.select('#weeklyChart').selectAll('*').remove();
|
||||
d3.select('#weeklyChart')
|
||||
.append('div')
|
||||
.html(`
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__text">
|
||||
No messages sent in the last 7 days
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
// Hide the subtitle
|
||||
if (subTitle) {
|
||||
subTitle.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// If there are messages, create the chart and table
|
||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => console.error('Error fetching daily stats:', error));
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
|
||||
|
||||
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
|
||||
var minSentPercentage = 0.02; // Minimum width as a percentage of total messages (1% in this case)
|
||||
var minSentPercentage = (sms_sent === 0) ? 0 : 0.02;
|
||||
var minSentValue = totalMessages * minSentPercentage;
|
||||
var displaySent = Math.max(sms_sent, minSentValue);
|
||||
var displayRemaining = totalMessages - displaySent;
|
||||
|
||||
4
app/assets/js/setTimezone.js
Normal file
@@ -0,0 +1,4 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
document.cookie = `timezone=${timeZone}; path=/`;
|
||||
})
|
||||
BIN
app/assets/pdf/tcpa_overview.pdf
Normal file
@@ -9,6 +9,9 @@ $failed: color('gray-cool-20');
|
||||
&.usage {
|
||||
height: units(4);
|
||||
}
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
#totalMessageChartContainer {
|
||||
|
||||
@@ -671,3 +671,229 @@ details form {
|
||||
border: 1px solid #face00;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.usa-prose.margin-top-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ol.best-practices-list {
|
||||
counter-reset: item;
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
ol.best-practices-list.set-two {
|
||||
counter-reset: item 6;
|
||||
}
|
||||
|
||||
ol.best-practices-list.set-three {
|
||||
counter-reset: item 10;
|
||||
}
|
||||
|
||||
ol.best-practices-list li {
|
||||
counter-increment: item;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
ol.best-practices-list li::before {
|
||||
content: counter(item);
|
||||
background-color: #005ea2;
|
||||
color: white;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
li.best-practices {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
div.best-practices {
|
||||
height: 400px
|
||||
}
|
||||
|
||||
$do-dont-color-do: "green-cool-50v";
|
||||
$do-dont-color-dont: "red-cool-50v";
|
||||
$do-dont-color-border: "gray-cool-20";
|
||||
$do-dont-padding: 3;
|
||||
$do-dont-top-bar-width: 1;
|
||||
|
||||
.do-dont {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-top: units(2);
|
||||
}
|
||||
|
||||
.do-dont__do,
|
||||
.do-dont__dont {
|
||||
background-color: color("white");
|
||||
border: 1px solid color($do-dont-color-border);
|
||||
flex: 1 0 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.do-dont__do {
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
.do-dont__do:before,
|
||||
.do-dont__dont:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: units($do-dont-top-bar-width);
|
||||
width: calc(100% + 2px);
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.do-dont__do:before {
|
||||
background-color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont:before {
|
||||
background-color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__heading {
|
||||
// @include typeset("lang", "lg", 2);
|
||||
align-items: center;
|
||||
border-bottom: 1px solid color($do-dont-color-border);
|
||||
display: flex;
|
||||
font-weight: fw("bold");
|
||||
margin: 0;
|
||||
padding: units(4) units($do-dont-padding) units($do-dont-padding);
|
||||
|
||||
.usa-icon {
|
||||
@include u-square(3);
|
||||
margin-right: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.do-dont__do .do-dont__heading {
|
||||
color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont .do-dont__heading {
|
||||
color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__content {
|
||||
padding: 0 units($do-dont-padding) units($do-dont-padding) units($do-dont-padding);
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
li+li {
|
||||
margin-top: units(1);
|
||||
}
|
||||
}
|
||||
|
||||
.site-note {
|
||||
background-color: #dfe1e2;
|
||||
max-width: 72ex;
|
||||
padding: .5rem;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.svg-circle-icon {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: text-bottom;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.height-phone-lg {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 758px) {
|
||||
.best-practices-flex-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-content:hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.3s ease, background-color 0.3s ease,
|
||||
}
|
||||
|
||||
li.linked-card:hover > div:first-child:hover {
|
||||
border-color: #005ea2;
|
||||
}
|
||||
|
||||
li.linked-card:hover h4,
|
||||
li.linked-card:hover p,
|
||||
li.linked-card:hover svg,
|
||||
.best-practices_card_img:hover{
|
||||
color: #005ea2;
|
||||
}
|
||||
|
||||
.best-practices_card_img {
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.best-practices_link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.usa-link--downloadable {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.usa-link--downloadable::after {
|
||||
background-image: url(../img/material-icons/download.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 1.75ex 1.75ex;
|
||||
height: 1.75ex;
|
||||
width: 1.75ex;
|
||||
content: "";
|
||||
display: inline;
|
||||
margin-top: .7ex;
|
||||
margin-left: 2px;
|
||||
padding-left: 1.75ex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.usa-link--downloadable::after {
|
||||
background: 0 0;
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: url(../img/material-icons/download.svg), linear-gradient(transparent, transparent);
|
||||
mask-image: url(../img/material-icons/download.svg), linear-gradient(transparent, transparent);
|
||||
-webkit-mask-position: center center;
|
||||
mask-position: center center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 1.75ex 1.75ex;
|
||||
mask-size: 1.75ex 1.75ex;
|
||||
}
|
||||
|
||||
.usa-sidenav__sublist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.usa-sidenav__item:hover .usa-sidenav__sublist,
|
||||
.usa-sidenav__item:focus-within .usa-sidenav__sublist {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.usa-sidenav__item a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ $path: '/static/images/';
|
||||
// Dependencies from GOV.UK Frontend, packaged to be specific to this application
|
||||
@import './govuk-frontend/all';
|
||||
|
||||
// Custom overrides
|
||||
.govuk-link {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Specific to this application
|
||||
@import 'local/typography';
|
||||
@import 'grids';
|
||||
|
||||
@@ -8,6 +8,7 @@ from notifications_utils import DAILY_MESSAGE_LIMIT
|
||||
|
||||
|
||||
class Config(object):
|
||||
SIMULATED_SMS_NUMBERS = ("+14254147755", "+14254147167")
|
||||
NOTIFY_APP_NAME = "admin"
|
||||
NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development")
|
||||
API_HOST_NAME = getenv("API_HOST_NAME", "localhost")
|
||||
@@ -86,6 +87,9 @@ class Config(object):
|
||||
"tts-benefits-studio@gsa.gov",
|
||||
],
|
||||
}
|
||||
FEATURE_BEST_PRACTICES_ENABLED = (
|
||||
getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true"
|
||||
)
|
||||
|
||||
|
||||
def _s3_credentials_from_env(bucket_prefix):
|
||||
|
||||
@@ -14,7 +14,7 @@ Learn how to [personalize messages](/using-notify/guidance) to increase response
|
||||
Learn about message _parts_ and [how limits are calculated](/using-notify/pricing).
|
||||
|
||||
5. ## Start sending messages
|
||||
To remove the restrictions of Trial Mode and begin sending messages to clients complete the [Live Campaign Form](https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL).
|
||||
To remove the restrictions of Trial Mode and begin sending messages to clients complete the <a class="usa-link usa-link--external" href="https://docs.google.com/forms/d/1fnaBtxuGf3q-OdGVyt2LqBKvp9_P21kmKJa0yIK8rWM/edit">Go-Live Form</a>.
|
||||
We’ll respond within one business day.
|
||||
|
||||
### Questions?
|
||||
|
||||
@@ -16,6 +16,7 @@ from app import (
|
||||
)
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
||||
from app.main import main
|
||||
from app.main.views.user_profile import set_timezone
|
||||
from app.statistics_utils import get_formatted_percentage
|
||||
from app.utils import (
|
||||
DELIVERED_STATUSES,
|
||||
@@ -39,6 +40,7 @@ def old_service_dashboard(service_id):
|
||||
@main.route("/services/<uuid:service_id>")
|
||||
@user_has_permissions()
|
||||
def service_dashboard(service_id):
|
||||
|
||||
if session.get("invited_user_id"):
|
||||
session.pop("invited_user_id", None)
|
||||
session["service_id"] = service_id
|
||||
@@ -323,13 +325,19 @@ def aggregate_template_usage(template_statistics, sort_key="count"):
|
||||
key=lambda x: x["template_id"],
|
||||
):
|
||||
template_stats = list(v)
|
||||
|
||||
first_stat = template_stats[0] if template_stats else None
|
||||
templates.append(
|
||||
{
|
||||
"template_id": k,
|
||||
"template_name": template_stats[0]["template_name"],
|
||||
"template_type": template_stats[0]["template_type"],
|
||||
"template_name": first_stat.get("template_name"),
|
||||
"template_type": first_stat.get("template_type"),
|
||||
"count": sum(s["count"] for s in template_stats),
|
||||
"created_by": first_stat.get("created_by"),
|
||||
"created_by_id": first_stat.get("created_by_id"),
|
||||
"last_used": first_stat.get("last_used"),
|
||||
"status": first_stat.get("status"),
|
||||
"template_folder": first_stat.get("template_folder"),
|
||||
"template_folder_id": first_stat.get("template_folder_id"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -402,11 +410,13 @@ def get_dashboard_partials(service_id):
|
||||
|
||||
|
||||
def get_dashboard_totals(statistics):
|
||||
|
||||
for msg_type in statistics.values():
|
||||
msg_type["failed_percentage"] = get_formatted_percentage(
|
||||
msg_type["failed"], msg_type["requested"]
|
||||
)
|
||||
msg_type["show_warning"] = float(msg_type["failed_percentage"]) > 3
|
||||
|
||||
return statistics
|
||||
|
||||
|
||||
@@ -465,6 +475,8 @@ def get_months_for_financial_year(year, time_format="%B"):
|
||||
|
||||
|
||||
def get_current_month_for_financial_year(year):
|
||||
# Setting the timezone here because we need to set it somewhere.
|
||||
set_timezone()
|
||||
current_month = datetime.now().month
|
||||
return current_month
|
||||
|
||||
|
||||
@@ -1,31 +1,58 @@
|
||||
import os
|
||||
import secrets
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import abort, current_app, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import status_api_client
|
||||
from app import redis_client, status_api_client
|
||||
from app.formatters import apply_html_class, convert_markdown_template
|
||||
from app.main import main
|
||||
from app.main.views.pricing import CURRENT_SMS_RATE
|
||||
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
|
||||
from app.main.views.sub_navigation_dictionaries import (
|
||||
best_practices_nav,
|
||||
features_nav,
|
||||
using_notify_nav,
|
||||
)
|
||||
from app.utils.user import user_is_logged_in
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
|
||||
# Hook to check for guidance routes
|
||||
@main.before_request
|
||||
def check_guidance_feature():
|
||||
current_app.logger.warning("best practices 1234")
|
||||
current_app.logger.warning(current_app.config["FEATURE_BEST_PRACTICES_ENABLED"])
|
||||
if (
|
||||
request.path.startswith("/best-practices")
|
||||
and not current_app.config["FEATURE_BEST_PRACTICES_ENABLED"]
|
||||
):
|
||||
abort(404)
|
||||
|
||||
|
||||
@main.route("/")
|
||||
def index():
|
||||
if current_user and current_user.is_authenticated:
|
||||
return redirect(url_for("main.choose_account"))
|
||||
token = generate_token(
|
||||
|
||||
# make and store the state
|
||||
state = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state)
|
||||
|
||||
# make and store the nonce
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce)
|
||||
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
# handle unit tests
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", token)
|
||||
url = url.replace("STATE", token)
|
||||
url = url.replace("NONCE", nonce)
|
||||
url = url.replace("STATE", state)
|
||||
return render_template(
|
||||
"views/signedout.html",
|
||||
sms_rate=CURRENT_SMS_RATE,
|
||||
@@ -178,12 +205,78 @@ def trial_mode_new():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices")
|
||||
@user_is_logged_in
|
||||
def best_practices():
|
||||
return render_template(
|
||||
"views/best-practices/best-practices.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/clear-goals")
|
||||
@user_is_logged_in
|
||||
def clear_goals():
|
||||
return render_template(
|
||||
"views/best-practices/clear-goals.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/rules-and-regulations")
|
||||
@user_is_logged_in
|
||||
def rules_and_regulations():
|
||||
return render_template(
|
||||
"views/best-practices/rules-and-regulations.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/establish-trust")
|
||||
@user_is_logged_in
|
||||
def establish_trust():
|
||||
return render_template(
|
||||
"views/best-practices/establish-trust.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/write-for-action")
|
||||
@user_is_logged_in
|
||||
def write_for_action():
|
||||
return render_template(
|
||||
"views/best-practices/write-for-action.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/multiple-languages")
|
||||
@user_is_logged_in
|
||||
def multiple_languages():
|
||||
return render_template(
|
||||
"views/best-practices/multiple-languages.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/benchmark-performance")
|
||||
@user_is_logged_in
|
||||
def benchmark_performance():
|
||||
return render_template(
|
||||
"views/best-practices/benchmark-performance.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/using-notify/guidance")
|
||||
@user_is_logged_in
|
||||
def guidance_index():
|
||||
return render_template(
|
||||
"views/guidance/index.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
feature_best_practices_enabled=current_app.config[
|
||||
"FEATURE_BEST_PRACTICES_ENABLED"
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,14 +17,12 @@ def accept_invite(token):
|
||||
and current_user.email_address.lower() != invited_user.email_address.lower()
|
||||
):
|
||||
message = Markup(
|
||||
"""
|
||||
You’re signed in as {}.
|
||||
f"""
|
||||
You’re signed in as {current_user.email_address}.
|
||||
This invite is for another email address.
|
||||
<a href={} class="usa-link">Sign out</a>
|
||||
<a href={url_for("main.sign_out")} class="usa-link">Sign out</a>
|
||||
and click the link again to accept this invite.
|
||||
""".format(
|
||||
current_user.email_address, url_for("main.sign_out")
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
flash(message=message)
|
||||
|
||||
@@ -77,6 +77,11 @@ def invite_user(service_id, user_id=None):
|
||||
form.login_authentication.data = "sms_auth"
|
||||
|
||||
if form.validate_on_submit():
|
||||
if not form.permissions or len(form.permissions) == 0:
|
||||
return render_template(
|
||||
"views/user-has-no-permissions.html",
|
||||
user_to_invite=user_to_invite,
|
||||
)
|
||||
email_address = form.email_address.data
|
||||
invited_user = InvitedUser.create(
|
||||
current_user.id,
|
||||
|
||||
@@ -10,10 +10,12 @@ from flask import (
|
||||
stream_with_context,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
|
||||
from app import current_service, job_api_client, notification_api_client
|
||||
from app.main import main
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_TEST
|
||||
from app.s3_client.s3_csv_client import delete_report, report_upload
|
||||
from app.utils import (
|
||||
DELIVERED_STATUSES,
|
||||
FAILURE_STATUSES,
|
||||
@@ -144,6 +146,36 @@ def download_notifications_csv(service_id):
|
||||
file_time = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
|
||||
file_time = f"{file_time} {get_user_preferred_timezone()}"
|
||||
|
||||
csv = generate_notifications_csv(
|
||||
service_id=service_id,
|
||||
job_id=None,
|
||||
status=filter_args.get("status"),
|
||||
page=request.args.get("page", 1),
|
||||
page_size=10000,
|
||||
format_for_csv=True,
|
||||
template_type=filter_args.get("message_type"),
|
||||
limit_days=service_data_retention_days,
|
||||
)
|
||||
|
||||
# START asynchronous reporting block
|
||||
csv_file = "".join(csv)
|
||||
file_location = f"reports/{service_id}/{current_user.id}/{service_data_retention_days}/report.csv"
|
||||
|
||||
# TODO these are some capabilities we will probably need when
|
||||
# report generation becomes asynchronous.
|
||||
|
||||
# old_content = report_download(file_location)
|
||||
# current_app.logger.info(f"OLD CONTENT IS {old_content}")
|
||||
# reports = get_downloadable_reports(current_user.id, service_id)
|
||||
|
||||
# TODO these are to support asynchronous report generation.
|
||||
# Leaving them commented in so they get exercised.
|
||||
delete_report(file_location)
|
||||
report_upload(file_location, csv_file)
|
||||
# END asynchronous reporting block
|
||||
|
||||
# TODO eventually we want to remove this, when reports become fully asynchronous
|
||||
# The UI should be retrieving the report elsewhere via the download_report() method call
|
||||
return Response(
|
||||
stream_with_context(
|
||||
generate_notifications_csv(
|
||||
|
||||
@@ -5,7 +5,16 @@ from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
from flask import Response, abort, flash, render_template, request, url_for
|
||||
from flask import (
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import (
|
||||
@@ -25,6 +34,7 @@ from app.main.forms import (
|
||||
DateFilterForm,
|
||||
RequiredDateFilterForm,
|
||||
)
|
||||
from app.main.views.send import _send_notification
|
||||
from app.statistics_utils import (
|
||||
get_formatted_percentage,
|
||||
get_formatted_percentage_two_dp,
|
||||
@@ -771,3 +781,80 @@ def _get_user_row(r):
|
||||
row.append(r["password_changed_at"])
|
||||
row.append(r["state"])
|
||||
return row
|
||||
|
||||
|
||||
@main.route(
|
||||
"/platform-admin/load-test",
|
||||
methods=["POST", "GET"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def load_test():
|
||||
"""
|
||||
The load test assumes that a service called 'Test service' exists. It will make
|
||||
the platform admin a member of this service if the platform is not already. All
|
||||
messagese will be sent in this service.
|
||||
"""
|
||||
service = _find_load_test_service()
|
||||
_prepare_load_test_service(service)
|
||||
example_template = _find_example_template(service)
|
||||
|
||||
# Simulated success
|
||||
for _ in range(0, 250):
|
||||
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][0]
|
||||
session["placeholders"] = {
|
||||
"day of week": "Monday",
|
||||
"color": "blue",
|
||||
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][0],
|
||||
}
|
||||
_send_notification(service["id"], example_template["id"])
|
||||
# Simulated failure
|
||||
for _ in range(0, 250):
|
||||
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][1]
|
||||
session["placeholders"] = {
|
||||
"day of week": "Wednesday",
|
||||
"color": "orange",
|
||||
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][1],
|
||||
}
|
||||
_send_notification(service["id"], example_template["id"])
|
||||
|
||||
# For now, just redirect to the splash page so we know it's done
|
||||
return render_template(
|
||||
"views/platform-admin/splash-page.html",
|
||||
)
|
||||
|
||||
|
||||
def _find_example_template(service):
|
||||
templates = service_api_client.get_service_templates(service["id"])
|
||||
templates = templates["data"]
|
||||
for template in templates:
|
||||
# template = json.loads(template)
|
||||
if template["name"] == "Example text message template":
|
||||
return template
|
||||
|
||||
raise Exception("Could not find example template for load test")
|
||||
|
||||
|
||||
def _find_load_test_service():
|
||||
services = service_api_client.find_services_by_name("Test service")
|
||||
services = services["data"]
|
||||
|
||||
for service in services:
|
||||
if service["name"] == "Test service":
|
||||
return service
|
||||
|
||||
raise Exception("Could not find 'Test service' for load test")
|
||||
|
||||
|
||||
def _prepare_load_test_service(service):
|
||||
users = user_api_client.get_all_users()
|
||||
for user in users:
|
||||
if user["platform_admin"] == "t":
|
||||
try:
|
||||
user_api_client.add_user_to_service(
|
||||
service["id"], user["id"], ["send messages"]
|
||||
)
|
||||
except Exception:
|
||||
current_app.logger.exception(
|
||||
"Couldnt add user, may already be part of service"
|
||||
)
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import base64
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import (
|
||||
abort,
|
||||
@@ -161,17 +161,29 @@ def set_up_your_profile():
|
||||
debug_msg(f"Enter set_up_your_profile with request.args {request.args}")
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
||||
if state != stored_state:
|
||||
current_app.logger.error(f"State Error: {state} != {stored_state}")
|
||||
abort(403)
|
||||
|
||||
login_gov_error = request.args.get("error")
|
||||
|
||||
if redis_client.get(f"invitedata-{state}") is None:
|
||||
access_token = sign_in._get_access_token(code, state)
|
||||
user_email = redis_client.get(f"user_email-{state}")
|
||||
user_uuid = redis_client.get(f"user_uuid-{state}")
|
||||
|
||||
new_user = user_email is None or user_uuid is None
|
||||
|
||||
if new_user: # invite path
|
||||
access_token = sign_in._get_access_token(code)
|
||||
|
||||
debug_msg("Got the access token for login.gov")
|
||||
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
|
||||
debug_msg(
|
||||
f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov"
|
||||
)
|
||||
invite_data = state.encode("utf8")
|
||||
invite_data = base64.b64decode(invite_data)
|
||||
invite_data = redis_client.get(f"invitedata-{state}")
|
||||
invite_data = json.loads(invite_data)
|
||||
debug_msg(f"final state {invite_data}")
|
||||
invited_user_id = invite_data["invited_user_id"]
|
||||
@@ -193,10 +205,7 @@ def set_up_your_profile():
|
||||
|
||||
form = SetupUserProfileForm()
|
||||
|
||||
if (
|
||||
form.validate_on_submit()
|
||||
and redis_client.get(f"invitedata-{state}") is not None
|
||||
):
|
||||
if form.validate_on_submit() and not new_user:
|
||||
invite_data, user_email, user_uuid, invited_user_email_address = (
|
||||
get_invite_data_from_redis(state)
|
||||
)
|
||||
@@ -221,6 +230,7 @@ def set_up_your_profile():
|
||||
activate_user(user["id"])
|
||||
debug_msg("activated user")
|
||||
usr = User.from_id(user["id"])
|
||||
|
||||
usr.add_to_service(
|
||||
invite_data["service_id"],
|
||||
invite_data["permissions"],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import redirect
|
||||
from flask import send_from_directory
|
||||
|
||||
from app.main import main
|
||||
|
||||
@@ -6,6 +6,4 @@ from app.main import main
|
||||
@main.route("/.well-known/security.txt", methods=["GET"])
|
||||
@main.route("/security.txt", methods=["GET"])
|
||||
def security_policy():
|
||||
# See GDS Way security policy which this implements
|
||||
# https://gds-way.cloudapps.digital/standards/vulnerability-disclosure.html#vulnerability-disclosure-and-security-txt
|
||||
return redirect("https://vdp.cabinetoffice.gov.uk/.well-known/security.txt")
|
||||
return send_from_directory(".well-known", "security.txt")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from string import ascii_uppercase
|
||||
@@ -33,6 +34,7 @@ from app.main.forms import (
|
||||
SetSenderForm,
|
||||
get_placeholder_form_instance,
|
||||
)
|
||||
from app.main.views.user_profile import set_timezone
|
||||
from app.models.user import Users
|
||||
from app.s3_client.s3_csv_client import (
|
||||
get_csv_metadata,
|
||||
@@ -800,9 +802,15 @@ def get_skip_link(step_index, template):
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_one_off_to_myself(service_id, template_id):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
current_app.logger.info("Send one off to myself")
|
||||
try:
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
except Exception:
|
||||
current_app.logger.exception("Couldnt get template for one off")
|
||||
# Use 406 just because we're limited to certain codes here and it will point us back to a problem here
|
||||
abort(406)
|
||||
|
||||
if db_template["template_type"] not in ("sms", "email"):
|
||||
abort(404)
|
||||
@@ -941,8 +949,8 @@ def preview_notification(service_id, template_id):
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_notification(service_id, template_id):
|
||||
scheduled_for = session.pop("scheduled_for", "")
|
||||
recipient = get_recipient()
|
||||
|
||||
if not recipient:
|
||||
return redirect(
|
||||
url_for(
|
||||
@@ -951,53 +959,10 @@ def send_notification(service_id, template_id):
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
upload_id = _send_notification(service_id, template_id)
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
for k, v in session["placeholders"].items():
|
||||
keys.append(k)
|
||||
values.append(v)
|
||||
|
||||
data = ",".join(keys)
|
||||
vals = ",".join(values)
|
||||
data = f"{data}\r\n{vals}"
|
||||
|
||||
filename = (
|
||||
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
|
||||
)
|
||||
my_data = {"filename": filename, "template_id": template_id, "data": data}
|
||||
upload_id = s3upload(service_id, my_data)
|
||||
|
||||
# To debug messages that the user reports have not been sent, we log
|
||||
# the csv filename and the job id. The user will give us the file name,
|
||||
# so we can search on that to obtain the job id, which we can use elsewhere
|
||||
# on the API side to find out what happens to the message.
|
||||
current_app.logger.info(
|
||||
hilite(
|
||||
f"One-off file: {filename} job_id: {upload_id} s3 location: service-{service_id}-notify/{upload_id}.csv"
|
||||
)
|
||||
)
|
||||
|
||||
form = CsvUploadForm()
|
||||
form.file.data = my_data
|
||||
form.file.name = filename
|
||||
|
||||
check_message_output = check_messages(service_id, template_id, upload_id, 2)
|
||||
if "You cannot send to" in check_message_output:
|
||||
return check_messages(service_id, template_id, upload_id, 2)
|
||||
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for=scheduled_for,
|
||||
template_id=template_id,
|
||||
original_file_name=filename,
|
||||
notification_count=1,
|
||||
valid="True",
|
||||
)
|
||||
|
||||
session.pop("recipient")
|
||||
session.pop("placeholders")
|
||||
session.pop("recipient", "")
|
||||
session.pop("placeholders", "")
|
||||
|
||||
# We have to wait for the job to run and create the notification in the database
|
||||
time.sleep(0.1)
|
||||
@@ -1045,6 +1010,63 @@ def send_notification(service_id, template_id):
|
||||
)
|
||||
|
||||
|
||||
def _send_notification(service_id, template_id):
|
||||
scheduled_for = session.pop("scheduled_for", "")
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
# Guarantee that the real phone number comes last, because some
|
||||
# users will have placeholders like "add your second phone number"
|
||||
# or something like as custom placeholders.
|
||||
for k, v in session["placeholders"].items():
|
||||
if k != "phone number":
|
||||
keys.append(k)
|
||||
values.append(v)
|
||||
if "phone number" in session["placeholders"].keys():
|
||||
keys.append("phone number")
|
||||
values.append(session["placeholders"]["phone number"])
|
||||
|
||||
data = ",".join(keys)
|
||||
vals = ",".join(values)
|
||||
data = f"{data}\r\n{vals}"
|
||||
filename = (
|
||||
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
|
||||
)
|
||||
my_data = {"filename": filename, "template_id": template_id, "data": data}
|
||||
upload_id = s3upload(service_id, my_data)
|
||||
# To debug messages that the user reports have not been sent, we log
|
||||
# the csv filename and the job id. The user will give us the file name,
|
||||
# so we can search on that to obtain the job id, which we can use elsewhere
|
||||
# on the API side to find out what happens to the message.
|
||||
current_app.logger.info(
|
||||
hilite(
|
||||
f"One-off file: {filename} job_id: {upload_id} s3 location: {service_id}-service-notify/{upload_id}.csv"
|
||||
)
|
||||
)
|
||||
|
||||
# For load testing we want to skip these checks. They are doing some fine-grained
|
||||
# comparison about what is in the preview, but the load test just blast messages
|
||||
# and doesn't care about the preview.
|
||||
if os.getenv("NOTIFY_ENVIRONMENT") not in ("development", "staging", "demo"):
|
||||
form = CsvUploadForm()
|
||||
form.file.data = my_data
|
||||
form.file.name = filename
|
||||
check_message_output = check_messages(service_id, template_id, upload_id, 2)
|
||||
if "You cannot send to" in check_message_output:
|
||||
return check_messages(service_id, template_id, upload_id, 2)
|
||||
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for=scheduled_for,
|
||||
template_id=template_id,
|
||||
original_file_name=filename,
|
||||
notification_count=1,
|
||||
valid="True",
|
||||
)
|
||||
return upload_id
|
||||
|
||||
|
||||
def get_email_reply_to_address_from_session():
|
||||
if session.get("sender_id"):
|
||||
return current_service.get_email_reply_to_address(session["sender_id"])[
|
||||
@@ -1075,6 +1097,7 @@ def get_spreadsheet_column_headings_from_template(template):
|
||||
|
||||
|
||||
def get_recipient():
|
||||
set_timezone()
|
||||
if {"recipient", "placeholders"} - set(session.keys()):
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import unquote
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
@@ -16,19 +18,19 @@ from flask import (
|
||||
)
|
||||
from flask_login import current_user
|
||||
|
||||
from app import login_manager, user_api_client
|
||||
from app import login_manager, redis_client, user_api_client
|
||||
from app.main import main
|
||||
from app.main.views.index import error
|
||||
from app.main.views.verify import activate_user
|
||||
from app.models.user import User
|
||||
from app.utils import hide_from_search_engines
|
||||
from app.utils.login import is_safe_redirect_url
|
||||
from app.utils.login import get_id_token, is_safe_redirect_url
|
||||
from app.utils.time import is_less_than_days_ago
|
||||
from app.utils.user import is_gov_user
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
|
||||
def _reformat_keystring(orig):
|
||||
def _reformat_keystring(orig): # pragma: no cover
|
||||
arr = orig.split("-----")
|
||||
begin = arr[1]
|
||||
end = arr[3]
|
||||
@@ -37,7 +39,7 @@ def _reformat_keystring(orig):
|
||||
return new_keystring
|
||||
|
||||
|
||||
def _get_access_token(code, state):
|
||||
def _get_access_token(code): # pragma: no cover
|
||||
client_id = os.getenv("LOGIN_DOT_GOV_CLIENT_ID")
|
||||
access_token_url = os.getenv("LOGIN_DOT_GOV_ACCESS_TOKEN_URL")
|
||||
keystring = os.getenv("LOGIN_PEM")
|
||||
@@ -60,17 +62,28 @@ def _get_access_token(code, state):
|
||||
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
||||
headers = {"Authorization": "Bearer %s" % token}
|
||||
response = requests.post(url, headers=headers)
|
||||
if response.json().get("access_token") is None:
|
||||
# Capture the response json here so it hopefully shows up in error reports
|
||||
current_app.logger.error(
|
||||
|
||||
response_json = response.json()
|
||||
id_token = get_id_token(response_json)
|
||||
nonce = id_token["nonce"]
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
stored_nonce = redis_client.get(nonce_key).decode("utf8")
|
||||
|
||||
if nonce != stored_nonce:
|
||||
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
access_token = response_json["access_token"]
|
||||
except KeyError as e:
|
||||
current_app.logger.exception(
|
||||
f"Error when getting access token {response.json()} #notify-admin-1505"
|
||||
)
|
||||
raise KeyError(f"'access_token' {response.json()}")
|
||||
access_token = response.json()["access_token"]
|
||||
raise KeyError(f"'access_token' {response.json()}") from e
|
||||
return access_token
|
||||
|
||||
|
||||
def _get_user_email_and_uuid(access_token):
|
||||
def _get_user_email_and_uuid(access_token): # pragma: no cover
|
||||
headers = {"Authorization": "Bearer %s" % access_token}
|
||||
user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL")
|
||||
user_attributes = requests.get(
|
||||
@@ -82,10 +95,11 @@ def _get_user_email_and_uuid(access_token):
|
||||
return user_email, user_uuid
|
||||
|
||||
|
||||
def _do_login_dot_gov():
|
||||
def _do_login_dot_gov(): # $ pragma: no cover
|
||||
# start login.gov
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
|
||||
login_gov_error = request.args.get("error")
|
||||
|
||||
if login_gov_error:
|
||||
@@ -94,10 +108,15 @@ def _do_login_dot_gov():
|
||||
)
|
||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||
elif code and state:
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
||||
if state != stored_state:
|
||||
current_app.logger.error(f"State Error: {state} != {stored_state}")
|
||||
abort(403)
|
||||
|
||||
# activate the user
|
||||
try:
|
||||
access_token = _get_access_token(code, state)
|
||||
access_token = _get_access_token(code)
|
||||
user_email, user_uuid = _get_user_email_and_uuid(access_token)
|
||||
if not is_gov_user(user_email):
|
||||
current_app.logger.error(
|
||||
@@ -124,12 +143,13 @@ def _do_login_dot_gov():
|
||||
except BaseException as be: # noqa B036
|
||||
current_app.logger.error(f"Error signing in: {be} #notify-admin-1505 ")
|
||||
error(401)
|
||||
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
|
||||
# end login.gov
|
||||
|
||||
|
||||
def verify_email(user, redirect_url):
|
||||
def verify_email(user, redirect_url): # pragma: no cover
|
||||
user_api_client.send_verify_code(user["id"], "email", None, redirect_url)
|
||||
title = "Email resent" if request.args.get("email_resent") else "Check your email"
|
||||
redirect_url = request.args.get("next")
|
||||
@@ -138,19 +158,41 @@ def verify_email(user, redirect_url):
|
||||
)
|
||||
|
||||
|
||||
def _handle_e2e_tests(redirect_url):
|
||||
current_app.logger.warning("E2E TESTS ARE ENABLED.")
|
||||
current_app.logger.warning(
|
||||
"If you are getting a 404 on signin, comment out E2E vars in .env file!"
|
||||
)
|
||||
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
|
||||
activate_user(user["id"])
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
def _handle_e2e_tests(redirect_url): # pragma: no cover
|
||||
try:
|
||||
current_app.logger.warning("E2E TESTS ARE ENABLED.")
|
||||
current_app.logger.warning(
|
||||
"If you are getting a 404 on signin, comment out E2E vars in .env file!"
|
||||
)
|
||||
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
|
||||
activate_user(user["id"])
|
||||
|
||||
# Check if the redirect URL is present and safe before proceeding further
|
||||
if redirect_url and is_safe_redirect_url(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.show_accounts_or_dashboard",
|
||||
next="EMAIL_IS_OK",
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
stre = str(e)
|
||||
stre = stre.replace(" ", "_")
|
||||
# Trying to get a message back to playwright somehow since we can't raise an error
|
||||
return redirect(url_for(f"https://{stre}"))
|
||||
|
||||
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in():
|
||||
def sign_in(): # pragma: no cover
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
return _handle_e2e_tests(redirect_url)
|
||||
|
||||
# If we have to revalidated the email, send the message
|
||||
# via email and redirect to the "verify your email page"
|
||||
# and don't proceed further with login
|
||||
@@ -162,26 +204,29 @@ def sign_in():
|
||||
):
|
||||
return email_verify_template
|
||||
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
return _handle_e2e_tests(redirect_url)
|
||||
|
||||
if current_user and current_user.is_authenticated:
|
||||
if redirect_url and is_safe_redirect_url(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
|
||||
token = generate_token(
|
||||
state = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state)
|
||||
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce)
|
||||
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
# handle unit tests
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", token)
|
||||
url = url.replace("STATE", token)
|
||||
url = url.replace("NONCE", nonce)
|
||||
url = url.replace("STATE", state)
|
||||
|
||||
return render_template(
|
||||
"views/signin.html",
|
||||
again=bool(redirect_url),
|
||||
@@ -190,5 +235,5 @@ def sign_in():
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def sign_in_again():
|
||||
def sign_in_again(): # pragma: no cover
|
||||
return redirect(url_for("main.sign_in", next=request.path))
|
||||
|
||||
@@ -58,3 +58,46 @@ def using_notify_nav():
|
||||
# },
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def best_practices_nav():
|
||||
return [
|
||||
{
|
||||
"name": "Best Practices",
|
||||
"link": "main.best_practices",
|
||||
},
|
||||
{
|
||||
"name": "Clear goals",
|
||||
"link": "main.clear_goals",
|
||||
},
|
||||
{
|
||||
"name": "Rules and regulations",
|
||||
"link": "main.rules_and_regulations",
|
||||
},
|
||||
{
|
||||
"name": "Establish trust",
|
||||
"link": "main.establish_trust",
|
||||
"sub_navigation_items": [
|
||||
{
|
||||
"name": "Get the word out",
|
||||
"link": "main.establish_trust#get-the-word-out",
|
||||
},
|
||||
{
|
||||
"name": "As people receive texts",
|
||||
"link": "main.establish_trust#as-people-receive-texts",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Write for action",
|
||||
"link": "main.write_for_action",
|
||||
},
|
||||
{
|
||||
"name": "Multiple languages",
|
||||
"link": "main.multiple_languages",
|
||||
},
|
||||
{
|
||||
"name": "Benchmark performance",
|
||||
"link": "main.benchmark_performance",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -29,6 +29,7 @@ from app.main.forms import (
|
||||
TwoFactorForm,
|
||||
)
|
||||
from app.models.user import User
|
||||
from app.utils import hilite
|
||||
from app.utils.user import user_is_gov_user, user_is_logged_in
|
||||
from notifications_utils.url_safe_token import check_token
|
||||
|
||||
@@ -189,32 +190,19 @@ def user_profile_mobile_number_delete():
|
||||
@main.route("/user-profile/mobile-number/authenticate", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_mobile_number_authenticate():
|
||||
# Validate password for form
|
||||
def _check_password(pwd):
|
||||
return user_api_client.verify_password(current_user.id, pwd)
|
||||
|
||||
form = ConfirmPasswordForm(_check_password)
|
||||
|
||||
if NEW_MOBILE not in session:
|
||||
return redirect(url_for(".user_profile_mobile_number"))
|
||||
|
||||
if form.validate_on_submit():
|
||||
session[NEW_MOBILE_PASSWORD_CONFIRMED] = True
|
||||
current_user.send_verify_code(to=session[NEW_MOBILE])
|
||||
create_mobile_number_change_event(
|
||||
user_id=current_user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_mobile_number=current_user.mobile_number,
|
||||
new_mobile_number=session[NEW_MOBILE],
|
||||
)
|
||||
return redirect(url_for(".user_profile_mobile_number_confirm"))
|
||||
|
||||
return render_template(
|
||||
"views/user-profile/authenticate.html",
|
||||
thing="mobile number",
|
||||
form=form,
|
||||
back_link=url_for(".user_profile_mobile_number_confirm"),
|
||||
session[NEW_MOBILE_PASSWORD_CONFIRMED] = True
|
||||
current_user.send_verify_code(to=session[NEW_MOBILE])
|
||||
create_mobile_number_change_event(
|
||||
user_id=current_user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_mobile_number=current_user.mobile_number,
|
||||
new_mobile_number=session[NEW_MOBILE],
|
||||
)
|
||||
return redirect(url_for(".user_profile_mobile_number_confirm"))
|
||||
|
||||
|
||||
@main.route("/user-profile/mobile-number/confirm", methods=["GET", "POST"])
|
||||
@@ -288,3 +276,15 @@ def user_profile_disable_platform_admin_view():
|
||||
return render_template(
|
||||
"views/user-profile/disable-platform-admin-view.html", form=form
|
||||
)
|
||||
|
||||
|
||||
def set_timezone():
|
||||
# Cookie is set in dashboard.html on page load
|
||||
try:
|
||||
timezone = request.cookies.get("timezone", "US/Eastern")
|
||||
current_app.logger.debug(hilite(f"User's timezone is {timezone}"))
|
||||
serialized_user = current_user.serialize()
|
||||
if serialized_user["preferred_timezone"] is not timezone:
|
||||
current_user.update(preferred_timezone=timezone)
|
||||
except Exception:
|
||||
current_app.logger.exception(hilite("Can't get timezone"))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from flask import abort, current_app, request, session
|
||||
@@ -58,6 +59,7 @@ class User(JSONModel, UserMixin):
|
||||
super().__init__(_dict)
|
||||
self.permissions = _dict.get("permissions", {})
|
||||
self._platform_admin = _dict["platform_admin"]
|
||||
self.preferred_timezone = _dict.get("preferred_timezone", "America/New_York")
|
||||
|
||||
@classmethod
|
||||
def from_id(cls, user_id):
|
||||
@@ -199,7 +201,7 @@ class User(JSONModel, UserMixin):
|
||||
@property
|
||||
def is_gov_user(self):
|
||||
is_gov = is_gov_user(self.email_address)
|
||||
current_app.logger.info(f"User {self.id} is_gov_user: {is_gov}")
|
||||
# current_app.logger.info(f"User {self.id} is_gov_user: {is_gov}")
|
||||
return is_gov
|
||||
|
||||
@property
|
||||
@@ -208,9 +210,9 @@ class User(JSONModel, UserMixin):
|
||||
|
||||
@property
|
||||
def platform_admin(self):
|
||||
current_app.logger.warn(
|
||||
f"Checking User {self.id} for platform admin: {self._platform_admin}"
|
||||
)
|
||||
# current_app.logger.warning(
|
||||
# f"Checking User {self.id} for platform admin: {self._platform_admin}"
|
||||
# )
|
||||
return self._platform_admin and not session.get(
|
||||
"disable_platform_admin_view", False
|
||||
)
|
||||
@@ -218,6 +220,14 @@ class User(JSONModel, UserMixin):
|
||||
def has_permissions(
|
||||
self, *permissions, restrict_admin_usage=False, allow_org_user=False
|
||||
):
|
||||
# TODO need this for load test, but breaks unit tests
|
||||
if self.platform_admin and os.getenv("NOTIFY_ENVIRONMENT") in (
|
||||
"development",
|
||||
"staging",
|
||||
"demo",
|
||||
):
|
||||
return True
|
||||
|
||||
unknown_permissions = set(permissions) - all_ui_permissions
|
||||
if unknown_permissions:
|
||||
raise TypeError(
|
||||
@@ -232,26 +242,31 @@ class User(JSONModel, UserMixin):
|
||||
# we shouldn't have any pages that require permissions, but don't specify a service or organization.
|
||||
# use @user_is_platform_admin for platform admin only pages
|
||||
# raise NotImplementedError
|
||||
current_app.logger.warn(f"VIEW ARGS ARE {request.view_args}")
|
||||
# current_app.logger.warning(f"VIEW ARGS ARE {request.view_args}")
|
||||
pass
|
||||
|
||||
log_msg = f"has_permissions user: {self.id} service: {service_id}"
|
||||
# platform admins should be able to do most things (except eg send messages, or create api keys)
|
||||
if self.platform_admin and not restrict_admin_usage:
|
||||
current_app.logger.warn(f"{log_msg} true because user is platform_admin")
|
||||
current_app.logger.debug(
|
||||
"has_permissions is true because user is platform_admin"
|
||||
)
|
||||
return True
|
||||
|
||||
if org_id:
|
||||
value = self.belongs_to_organization(org_id)
|
||||
current_app.logger.warn(f"{log_msg} org: {org_id} returning {value}")
|
||||
current_app.logger.debug(
|
||||
f"has_permissions returns org: {org_id} returning {value}"
|
||||
)
|
||||
return value
|
||||
|
||||
if not permissions and self.belongs_to_service(service_id):
|
||||
current_app.logger.warn(f"{log_msg} True because belongs_to_service")
|
||||
current_app.logger.debug("has_permissions True because belongs_to_service")
|
||||
return True
|
||||
|
||||
if any(self.permissions_for_service(service_id) & set(permissions)):
|
||||
current_app.logger.warn(f"{log_msg} permissions valid")
|
||||
current_app.logger.debug(
|
||||
"has_permissions returns True because permissions valid"
|
||||
)
|
||||
return True
|
||||
|
||||
from app.models.service import Service
|
||||
@@ -259,7 +274,7 @@ class User(JSONModel, UserMixin):
|
||||
org_value = allow_org_user and self.belongs_to_organization(
|
||||
Service.from_id(service_id).organization_id
|
||||
)
|
||||
current_app.logger.warn(f"{log_msg} returning {org_value}")
|
||||
current_app.logger.debug(f"has_permissions returning {org_value}")
|
||||
return org_value
|
||||
|
||||
def permissions_for_service(self, service_id):
|
||||
@@ -267,7 +282,7 @@ class User(JSONModel, UserMixin):
|
||||
|
||||
def has_permission_for_service(self, service_id, permission):
|
||||
has_permission = permission in self.permissions_for_service(service_id)
|
||||
current_app.logger.warn(
|
||||
current_app.logger.debug(
|
||||
f"has_permission_for_service user: {self.id} service: {service_id} "
|
||||
f"permission: {permission} retuning {has_permission}"
|
||||
)
|
||||
@@ -548,17 +563,17 @@ class InvitedUser(JSONModel):
|
||||
return cls.by_id(invited_user_id) if invited_user_id else None
|
||||
|
||||
def has_permissions(self, *permissions):
|
||||
current_app.logger.warn(
|
||||
f"Checking invited user {self.id} for permissions: {permissions}"
|
||||
)
|
||||
# current_app.logger.warning(
|
||||
# f"Checking invited user {self.id} for permissions: {permissions}"
|
||||
# )
|
||||
if self.status == "cancelled":
|
||||
return False
|
||||
return set(self.permissions) > set(permissions)
|
||||
|
||||
def has_permission_for_service(self, service_id, permission):
|
||||
current_app.logger.warn(
|
||||
f"Checking invited user {self.id} for permission: {permission} on service {service_id}"
|
||||
)
|
||||
# current_app.logger.warn(
|
||||
# f"Checking invited user {self.id} for permission: {permission} on service {service_id}"
|
||||
# )
|
||||
if self.status == "cancelled":
|
||||
return False
|
||||
return self.service == service_id and permission in self.permissions
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import json
|
||||
import secrets
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import current_app, request
|
||||
|
||||
from app import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
from app.utils.user_permissions import (
|
||||
all_ui_permissions,
|
||||
translate_permissions_from_ui_to_db,
|
||||
)
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
|
||||
class InviteApiClient(NotifyAdminAPIClient):
|
||||
@@ -32,8 +40,33 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
"folder_permissions": folder_permissions,
|
||||
}
|
||||
data = _attach_current_user(data)
|
||||
|
||||
# make and store the state
|
||||
state = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state)
|
||||
|
||||
# make and store the nonce
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce) # save the nonce to redis.
|
||||
|
||||
data["nonce"] = nonce # This is passed to api for the invite url.
|
||||
data["state"] = state # This is passed to api for the invite url.
|
||||
|
||||
resp = self.post(url=f"/service/{service_id}/invite", data=data)
|
||||
return resp["data"]
|
||||
|
||||
resp_data = resp["data"]
|
||||
invite_data_key = f"invitedata-{unquote(state)}"
|
||||
redis_invite_data = resp["invite"]
|
||||
redis_invite_data = json.dumps(redis_invite_data)
|
||||
redis_client.set(invite_data_key, redis_invite_data)
|
||||
|
||||
return resp_data
|
||||
|
||||
def get_invites_for_service(self, service_id):
|
||||
return self.get(f"/service/{service_id}/invite")["data"]
|
||||
@@ -64,7 +97,32 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
|
||||
|
||||
def resend_invite(self, service_id, invited_user_id):
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}/resend", data={})
|
||||
# make and store the state
|
||||
state = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state)
|
||||
|
||||
# make and store the nonce
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce)
|
||||
|
||||
data = {
|
||||
"nonce": nonce,
|
||||
"state": state,
|
||||
}
|
||||
resp = self.post(
|
||||
url=f"/service/{service_id}/invite/{invited_user_id}/resend", data=data
|
||||
)
|
||||
|
||||
invite_data_key = f"invitedata-{unquote(state)}"
|
||||
redis_invite_data = resp["invite"]
|
||||
redis_invite_data = json.dumps(redis_invite_data)
|
||||
redis_client.set(invite_data_key, redis_invite_data)
|
||||
|
||||
@cache.delete("service-{service_id}")
|
||||
@cache.delete("user-{invited_user_id}")
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
|
||||
ALLOWED_ATTRIBUTES = {
|
||||
@@ -109,13 +110,14 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
raise
|
||||
|
||||
def send_verify_code(self, user_id, code_type, to, next_string=None):
|
||||
|
||||
data = {"to": to}
|
||||
if next_string:
|
||||
data["next"] = next_string
|
||||
if code_type == "email":
|
||||
data["email_auth_link_host"] = self.admin_url
|
||||
endpoint = "/user/{0}/{1}-code".format(user_id, code_type)
|
||||
current_app.logger.warn(f"Sending verify_code {code_type} to {user_id}")
|
||||
current_app.logger.warn(hilite(f"Sending verify_code {code_type} to {user_id}"))
|
||||
self.post(endpoint, data=data)
|
||||
|
||||
def send_verify_email(self, user_id, to):
|
||||
@@ -166,7 +168,7 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
@cache.delete("user-{user_id}")
|
||||
def add_user_to_service(self, service_id, user_id, permissions, folder_permissions):
|
||||
# permissions passed in are the combined UI permissions, not DB permissions
|
||||
endpoint = "/service/{}/users/{}".format(service_id, user_id)
|
||||
endpoint = f"/service/{service_id}/users/{user_id}"
|
||||
data = {
|
||||
"permissions": [
|
||||
{"permission": x}
|
||||
|
||||
@@ -29,7 +29,10 @@ def get_s3_object(
|
||||
aws_secret_access_key=secret_key,
|
||||
region_name=region,
|
||||
)
|
||||
s3 = session.resource("s3", config=AWS_CLIENT_CONFIG)
|
||||
s3 = session.resource(
|
||||
"s3",
|
||||
config=AWS_CLIENT_CONFIG,
|
||||
)
|
||||
obj = s3.Object(bucket_name, filename)
|
||||
# This 'proves' that use of moto in the relevant tests in test_send.py
|
||||
# mocks everything related to S3. What you will see in the logs is:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import boto3
|
||||
from flask import current_app
|
||||
|
||||
from app.s3_client import (
|
||||
AWS_CLIENT_CONFIG,
|
||||
get_s3_contents,
|
||||
get_s3_metadata,
|
||||
get_s3_object,
|
||||
@@ -10,13 +13,13 @@ from app.s3_client import (
|
||||
)
|
||||
from notifications_utils.s3 import s3upload as utils_s3upload
|
||||
|
||||
FILE_LOCATION_STRUCTURE = "service-{}-notify/{}.csv"
|
||||
NEW_FILE_LOCATION_STRUCTURE = "{}-service-notify/{}.csv"
|
||||
|
||||
|
||||
def get_csv_location(service_id, upload_id):
|
||||
return (
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["bucket"],
|
||||
FILE_LOCATION_STRUCTURE.format(service_id, upload_id),
|
||||
NEW_FILE_LOCATION_STRUCTURE.format(service_id, upload_id),
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"],
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["secret_access_key"],
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["region"],
|
||||
@@ -33,6 +36,14 @@ def s3upload(service_id, filedata):
|
||||
bucket_name, file_location, access_key, secret_key, region = get_csv_location(
|
||||
service_id, upload_id
|
||||
)
|
||||
if bucket_name == "":
|
||||
exp_bucket = current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
|
||||
exp_region = current_app.config["CSV_UPLOAD_BUCKET"]["region"]
|
||||
tier = os.getenv("NOTIFY_ENVIRONMENT")
|
||||
raise Exception(
|
||||
f"NO BUCKET NAME SHOULD BE: {exp_bucket} WITH REGION {exp_region} TIER {tier}"
|
||||
)
|
||||
|
||||
utils_s3upload(
|
||||
filedata=filedata["data"],
|
||||
region=region,
|
||||
@@ -54,3 +65,70 @@ def set_metadata_on_csv_upload(service_id, upload_id, **kwargs):
|
||||
|
||||
def get_csv_metadata(service_id, upload_id):
|
||||
return get_s3_metadata(get_csv_upload(service_id, upload_id))
|
||||
|
||||
|
||||
def report_upload(file_location, report_content):
|
||||
bucket_name = current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
|
||||
access_key = current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"]
|
||||
secret_key = current_app.config["CSV_UPLOAD_BUCKET"]["secret_access_key"]
|
||||
region = current_app.config["CSV_UPLOAD_BUCKET"]["region"]
|
||||
|
||||
utils_s3upload(
|
||||
filedata=report_content,
|
||||
region=region,
|
||||
bucket_name=bucket_name,
|
||||
file_location=file_location,
|
||||
access_key=access_key,
|
||||
secret_key=secret_key,
|
||||
)
|
||||
current_app.logger.info(f"Succcessfully uploaded report to {file_location}")
|
||||
|
||||
|
||||
def report_download(file_location):
|
||||
current_app.logger.info(f"Downloading report from {file_location}")
|
||||
bucket_name = current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
|
||||
access_key = current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"]
|
||||
secret_key = current_app.config["CSV_UPLOAD_BUCKET"]["secret_access_key"]
|
||||
region = current_app.config["CSV_UPLOAD_BUCKET"]["region"]
|
||||
|
||||
return get_s3_contents(
|
||||
get_s3_object(bucket_name, file_location, access_key, secret_key, region)
|
||||
)
|
||||
|
||||
|
||||
def delete_report(file_location):
|
||||
current_app.logger.info(f"Deleting report from {file_location}")
|
||||
bucket_name = current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
|
||||
access_key = current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"]
|
||||
secret_key = current_app.config["CSV_UPLOAD_BUCKET"]["secret_access_key"]
|
||||
region = current_app.config["CSV_UPLOAD_BUCKET"]["region"]
|
||||
|
||||
obj = get_s3_object(bucket_name, file_location, access_key, secret_key, region)
|
||||
if obj is None:
|
||||
return None
|
||||
return obj.delete()
|
||||
|
||||
|
||||
def get_downloadable_reports(user_id, service_id):
|
||||
prefix = f"reports/{service_id}/{user_id}/"
|
||||
bucket_name = current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
|
||||
access_key = current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"]
|
||||
secret_key = current_app.config["CSV_UPLOAD_BUCKET"]["secret_access_key"]
|
||||
region = current_app.config["CSV_UPLOAD_BUCKET"]["region"]
|
||||
|
||||
session = boto3.Session(
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
region_name=region,
|
||||
)
|
||||
s3 = session.client(
|
||||
"s3",
|
||||
config=AWS_CLIENT_CONFIG,
|
||||
)
|
||||
|
||||
response = s3.list_objects_v2(Bucket=bucket_name, Prefix=prefix)
|
||||
object_keys = []
|
||||
if "Contents" in response:
|
||||
for obj in response["Contents"]:
|
||||
object_keys.append(obj["Key"])
|
||||
return object_keys
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% block per_page_title %}{% endblock %} – Notify.gov
|
||||
{% endblock %}
|
||||
</title>
|
||||
{% include "new/components/head.html" %}
|
||||
{% include "components/head.html" %}
|
||||
</head>
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||
@@ -32,8 +32,8 @@
|
||||
|
||||
|
||||
{% block header %}
|
||||
{% include 'new/components/usa_banner.html' %}
|
||||
{% include 'new/components/header.html' %}
|
||||
{% include 'components/usa_banner.html' %}
|
||||
{% include 'components/header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
@@ -47,15 +47,36 @@
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% block flash_messages %}
|
||||
{% include 'new/components/flash_messages.html' %}
|
||||
{% include 'components/flash_messages.html' %}
|
||||
{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
<div class="grid-row">
|
||||
{% if navigation_links %}
|
||||
<div class="tablet:grid-col-2 margin-bottom-4">
|
||||
{{ sub_navigation(navigation_links) }}
|
||||
<nav class="nav">
|
||||
<ul class="usa-sidenav">
|
||||
{% for item in navigation_links %}
|
||||
<li class="usa-sidenav__item">
|
||||
<a href="{{ url_for(item.link) }}"
|
||||
class="parent-link {% if item['link'] == request.endpoint %} usa-current {% endif %}"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
{% if item.sub_navigation_items %}
|
||||
<ul class="usa-sidenav__sublist" role="menu">
|
||||
{% for sub_item in item.sub_navigation_items %}
|
||||
<li role="menuitem">
|
||||
<a href="{{ url_for(sub_item.link.split('#')[0]) }}#{{ sub_item.link.split('#')[1] }}">{{ sub_item.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="tablet:grid-col-10 padding-left-4 usa-prose site-prose">
|
||||
<div class="tablet:grid-col-10 tablet:padding-left-4 usa-prose site-prose">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-10">
|
||||
{% endif %}
|
||||
@@ -78,11 +99,11 @@
|
||||
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||
{% endif %}
|
||||
|
||||
{% include "new/components/footer.html" %}
|
||||
{% include "components/footer.html" %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% block sessionUserWarning %}
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timerWarning">
|
||||
<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">
|
||||
|
||||
42
app/templates/components/best-practices/cards.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<ul class="usa-card-group">
|
||||
{% for item in card_contents %}
|
||||
<li class="usa-card grid-col tablet:grid-col-4 {% if item.link %}linked-card linked-content{% endif %}">
|
||||
<div class="usa-card__container">
|
||||
{% if item.card_heading %}
|
||||
<div class="usa-card__header">
|
||||
{% if item.link %}
|
||||
<a href="{{ item.link }}">
|
||||
{% endif %}
|
||||
<h3
|
||||
class="usa-card__heading {% if text_align != 'left' or text_align is not defined %}text-center{% else %}text-left{% endif %} {% if item.link %}linked-card{% endif %}">
|
||||
{{ item.card_heading }}
|
||||
</h3>
|
||||
{% if item.link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.svg_src or item.image_src%}
|
||||
<div class="usa-card__media usa-card__media--inset">
|
||||
{% if item.svg_src %}
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="best-practices_card_img">
|
||||
<use xlink:href="{{asset_url('images/best-practices-card-sprite.svg')}}#{{item.svg_src}}"></use>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{% if item.image_src %}
|
||||
<img class="best-practices_card_img" src="{{item.image_src}}" alt="{{ item.alt_text }}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.p_text %}
|
||||
<div class="usa-card__body">
|
||||
<p
|
||||
class="{% if text_align != 'left' or text_align is not defined %}text-center{% else %}text-left{% endif %} {% if item.link %}linked-card{% endif %}">
|
||||
{{item.p_text|safe}}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -0,0 +1,9 @@
|
||||
{% macro circle_number(number) %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="svg-circle-icon" width="24" height="24" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="37" stroke="#005ea2" stroke-width="7" fill="none" />
|
||||
<text x="50%" y="50%" text-anchor="middle" font-size="40" font-weight="bolder" fill="#005ea2" font-family="Arial"
|
||||
dy=".35em">
|
||||
{{ number }}
|
||||
</text>
|
||||
</svg>
|
||||
{% endmacro %}
|
||||
13
app/templates/components/best-practices/nav_breadcrumb.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<nav class="navigation-service usa-breadcrumb padding-top-0">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<span class="usa-breadcrumb__label"><a href="{{ url_for('.best_practices') }}"
|
||||
class="usa-link navigation-organization-link">Best Practices</a></span>
|
||||
</li>
|
||||
{% if title %}
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<span class="usa-breadcrumb__label">{{ title }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
@@ -1,2 +1,4 @@
|
||||
<a href="{%- if params.href %}{{ params.href }}{% else %}#{% endif -%}" class="usa-link usa-back-link display-inline-flex margin-bottom-3 {%- if params.classes %} {{ params.classes }}{% endif -%}"
|
||||
{%- for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}>{{ (params.html | safe if params.html else (params.text if params.text else 'Back')) }}</a>
|
||||
<nav class="usa-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="{{ 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.text or 'Back' }}</a>
|
||||
</nav>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# Footer
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the footer component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/footer).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
|
||||
See [options table](https://design-system.service.gov.uk/components/footer/#options-example-default) for details.
|
||||
@@ -1,115 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "meta",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Object containing options for the meta navigation.",
|
||||
"params": [
|
||||
{
|
||||
"name": "visuallyHiddenTitle",
|
||||
"type": "string",
|
||||
"description": "Title for a meta item section, which defaults to Support links"
|
||||
},
|
||||
{
|
||||
"name": "html",
|
||||
"type": "string",
|
||||
"description": "HTML to add to the meta section of the footer, which will appear below any links specified using meta.items."
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "Text to add to the meta section of the footer, which will appear below any links specified using meta.items. If meta.html is specified, this option is ignored."
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Array of items for use in the meta section of the footer.",
|
||||
"params": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item text in the meta section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "href",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item href attribute in the meta section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the anchor in the footer meta section."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "navigation",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Array of items for use in the navigation section of the footer.",
|
||||
"params": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Title for a section"
|
||||
},
|
||||
{
|
||||
"name": "columns",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"description": "Amount of columns to display items in navigation section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Array of items to display in the list in navigation section of the footer.",
|
||||
"params": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item text in the navigation section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "href",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item href attribute in the navigation section of the footer. Both `text` and `href` attributes need to be present to create a link."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the anchor in the footer navigation section."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "containerClasses",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes that can be added to the inner container, useful if you want to make the footer full width."
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the footer component container."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the footer component container."
|
||||
}
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
{% macro usaFooter(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
@@ -1,85 +0,0 @@
|
||||
<footer class="usa-footer usa-footer--big">
|
||||
{# <div class="grid-container usa-footer__return-to-top">
|
||||
<a href="#">Return to top</a>
|
||||
</div> #}
|
||||
<div class="usa-footer__primary-section">
|
||||
<div class="grid-container">
|
||||
<div class="padding-y-1">
|
||||
{% if params.meta %}
|
||||
<h2 class="usa-sr-only">{{ params.meta.visuallyHiddenTitle | default("Support links") }}</h2>
|
||||
{% if params.meta.items %}
|
||||
{% for item in params.meta.items %}
|
||||
<a class="usa-footer__link" href="{{ item.href }}" {% for attribute, value in item.attributes %}
|
||||
{{attribute}}="{{value}}" {% endfor %}>
|
||||
{{ item.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if params.meta.text or params.meta.html %}
|
||||
<div>
|
||||
{{ params.meta.html | safe if params.meta.html else params.meta.text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="usa-identifier site-identifier">
|
||||
<section class="usa-identifier__section usa-identifier__section--masthead" aria-label="Agency identifier,,,,,,">
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__logos"><a href="javascript:void(0);" class="usa-identifier__logo">
|
||||
<img class="usa-identifier__logo-img" src="{{ params.assetsPath | default('/static/images') }}/gsa-logo.svg" alt="GSA Logo" role="img" width="48"
|
||||
height="48">
|
||||
</a></div>
|
||||
<section class="usa-identifier__identity" aria-label="Agency description">
|
||||
<p class="usa-identifier__identity-domain">beta.notify.gov</p>
|
||||
<p class="usa-identifier__identity-disclaimer">An official website of the <a href="https://gsa.gov">General
|
||||
Services Administration</a></p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="usa-identifier__section usa-identifier__section--required-links" aria-label="Important links">
|
||||
<div class="usa-identifier__container">
|
||||
<ul class="usa-identifier__required-links-list">
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/about-us" class="usa-identifier__required-link">About GSA</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/website-information/accessibility-aids"
|
||||
class="usa-identifier__required-link">Accessibility support</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/freedom-of-information-act-foia"
|
||||
class="usa-identifier__required-link">FOIA requests</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/civil-rights-programs/notification-and-federal-employee-antidiscrimination-and-retaliation-act-of-2002"
|
||||
class="usa-identifier__required-link">No FEAR Act data</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsaig.gov/" class="usa-identifier__required-link">Office of the Inspector General</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/reports/budget-performance"
|
||||
class="usa-identifier__required-link">Performance reports</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/website-information/website-policies"
|
||||
class="usa-identifier__required-link">Privacy policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="usa-identifier__section usa-identifier__section--usagov"
|
||||
aria-label="Government information and services">
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__usagov-description">
|
||||
Looking for U.S. government information and services?
|
||||
</div>
|
||||
<a href="https://www.usa.gov/" class="usa-link">Visit USA.gov</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
# Header
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the header component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/header).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
|
||||
See [options table](https://design-system.service.gov.uk/components/header/#options-example-default) for details.
|
||||
@@ -1,88 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "homepageUrl",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The url of the homepage. Defaults to /"
|
||||
},
|
||||
{
|
||||
"name": "assetsPath",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The public path for the assets folder. If not provided it defaults to /assets/images"
|
||||
},
|
||||
{
|
||||
"name": "productName",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Header title that is placed next to GOV.UK. Used for product names (i.e. Pay, Verify)"
|
||||
},
|
||||
{
|
||||
"name": "serviceName",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Header title that is placed next to GOV.UK. Used for product names (i.e. Pay, Verify)"
|
||||
},
|
||||
{
|
||||
"name": "serviceUrl",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Url for the service name anchor."
|
||||
},
|
||||
{
|
||||
"name": "navigation",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of navigation item objects.",
|
||||
"params": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Text of the navigation item."
|
||||
},
|
||||
{
|
||||
"name": "href",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Url of the navigation item anchor. Both `href` and `text` attributes for navigation items need to be provided to create an item."
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Flag to mark the navigation item as active or not."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the navigation item anchor."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "navigationClasses",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes for the navigation section of the header."
|
||||
},
|
||||
{
|
||||
"name": "containerClasses",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes for the container, useful if you want to make the header fixed width."
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the header container."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the header container."
|
||||
}
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
{% macro usaHeader(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
@@ -1,105 +0,0 @@
|
||||
<!-- usa banner -->
|
||||
<section class="usa-banner site-banner" aria-label="Official website of the United States government,,,,,,">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
<div class="usa-banner__inner">
|
||||
<div class="grid-col-auto">
|
||||
<img aria-hidden="true" class="usa-banner__header-flag" src="/static/img/us_flag_small.png" alt="" />
|
||||
</div>
|
||||
<div class="grid-col-fill tablet:grid-col-auto" aria-hidden="true">
|
||||
<p class="usa-banner__header-text">An official website of the United States government</p>
|
||||
<p class="usa-banner__header-action">Here’s how you know</p>
|
||||
</div>
|
||||
<button type="button" class="usa-accordion__button usa-banner__button" aria-expanded="false" aria-controls="gov-banner">
|
||||
<span class="usa-banner__button-text">Here’s how you know</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="usa-banner__content usa-accordion__content" id="gov-banner" hidden="">
|
||||
<div class="grid-row grid-gap-lg">
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-dot-gov.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Official websites use .gov</strong>
|
||||
<br>
|
||||
A <strong>.gov</strong> website belongs to an official government organization in the United States.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-https.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Secure .gov websites use HTTPS</strong>
|
||||
<br>
|
||||
A <strong>lock</strong> ( <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" width="52" height="64" viewBox="0 0 52 64" class="usa-banner__lock-image" role="img" aria-labelledby="banner-lock-description" focusable="false"><title id="banner-lock-title">Lock</title><desc id="banner-lock-description">Locked padlock</desc><path fill="#000000" fill-rule="evenodd" d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"></path></svg></span> ) or <strong>https://</strong> means you’ve safely connected to the .gov website. Share sensitive information only on official, secure websites.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- usa header -->
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<image src="{{ params.assetsPath | default('/static/images') }}/notify-logo.png" alt="Notify.gov logo" xlink:href=""
|
||||
class="usa-flag-logo margin-right-1"></image>
|
||||
</a>
|
||||
</div>
|
||||
{% if params.navigation %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in params.navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% for item in params.secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- <section aria-label="Search component">
|
||||
<form class="usa-search usa-search--small margin-bottom-2" role="search">
|
||||
<label class="usa-sr-only" for="search-field">Search</label>
|
||||
<input class="usa-input" id="search-field" type="search" name="search" />
|
||||
<button class="usa-button" type="submit">
|
||||
<img src="/static/images/usa-icons-bg/search--white.svg" class="usa-search__submit-icon" alt="Search" />
|
||||
</button>
|
||||
</form>
|
||||
</section> -->
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,7 +1,3 @@
|
||||
<a href="{{ params.href | default('#content') }}" class="usa-skipnav{%- if params.classes %} {{ params.classes }}{% endif -%}"{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}>
|
||||
{{- params.html | safe if params.html else params.text -}}
|
||||
</a>
|
||||
|
||||
<a class="usa-skipnav " href="#main-content">
|
||||
Skip to main content
|
||||
</a>
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/static') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/static') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/static') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="manifest" href="{{ asset_url('images/site.webmanifest') }}">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<link href="{{ assetPath | default('/static') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
@@ -30,7 +30,7 @@
|
||||
<meta property="og:image" content="/static/images/notify-og-image.png">
|
||||
{# google #}
|
||||
<script type="text/javascript" src="{{ asset_url('js/gtm_head.js') }}"></script>
|
||||
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
|
||||
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" /> {# pragma: allowlist secret #}
|
||||
{% if g.hide_from_search_engines %}
|
||||
<meta name="robots" content="noindex" />
|
||||
{% endif %}
|
||||
@@ -1,4 +1,3 @@
|
||||
{# setting navigation and secondarynavigation #}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": header_navigation.is_selected('accounts-or-dashboard')},
|
||||
@@ -30,7 +29,6 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# usa header #}
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
@@ -50,7 +48,7 @@
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
<img src="{{ asset_url('img/usa-icons/close.svg') }}" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in navigation %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{% if help %}
|
||||
{% include 'partials/tour.html' %}
|
||||
{% else %}
|
||||
<nav class="nav margin-bottom-4">
|
||||
<nav id="nav-main-nav" aria-label="Main navigation" class="nav margin-bottom-4">
|
||||
<a class="usa-button margin-top-1 margin-bottom-5 width-full"
|
||||
href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a>
|
||||
<ul class="usa-sidenav">
|
||||
@@ -13,15 +13,6 @@
|
||||
{% if not current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
|
||||
{% endif %}
|
||||
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||
{% endif %} -->
|
||||
{% if current_user.has_permissions('manage_api_keys') %}
|
||||
<!-- <li><a class="usa-link{{ main_navigation.is_selected('api-integration') }}" href="{{ url_for('.api_integration', service_id=current_service.id) }}">API integration</a></li> -->
|
||||
{% endif %}
|
||||
{% elif current_user.has_permissions(allow_org_user=True) %}
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav class="nav margin-bottom-4">
|
||||
<nav id="nav-org-nav" aria-label="Organization navigation" class="nav margin-bottom-4">
|
||||
<ul class="usa-sidenav">
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Usage</a></li>
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
|
||||
@@ -1,3 +1,4 @@
|
||||
<nav id="nav-service-nav" aria-label="Service navigation">
|
||||
<div class="navigation-service margin-top-5 display-flex flex-align-end flex-justify border-bottom padding-bottom-1">
|
||||
{% if current_service.organization_id %}
|
||||
{% if current_user.platform_admin or
|
||||
@@ -13,3 +14,4 @@
|
||||
</div>
|
||||
<a href="{{ url_for('main.choose_account') }}" class="usa-link">Switch service</a>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
{# usa banner #}
|
||||
<section class="usa-banner site-banner" aria-label="Official website of the United States government,,,,,,">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
@@ -1,302 +0,0 @@
|
||||
{% extends "main_template.html" %}
|
||||
{% from "components/banner.html" import banner %}
|
||||
|
||||
{% block headIcons %}
|
||||
<link rel="shortcut icon" sizes="16x16 32x32 48x48" href="{{ asset_url('images/favicon.ico') }}" type="image/x-icon" />
|
||||
<link rel="mask-icon" href="{{ asset_url('images/usa-mask-icon.svg') }}" color="{{ themeColor | default('#F0F0F0') }}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset_url('images/apple-touch-icon.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ asset_url('images/apple-touch-icon.png') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" media="screen" href="{{ asset_url('css/styles.css') }}" />
|
||||
{% block extra_stylesheets %}
|
||||
{% endblock %}
|
||||
{% if g.hide_from_search_engines %}
|
||||
<meta name="robots" content="noindex" />
|
||||
{% endif %}
|
||||
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
|
||||
{% block meta_format_detection %}
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
{% endblock %}
|
||||
{% block meta %}
|
||||
<meta property="og:site_name" content="Notify.gov">
|
||||
<meta property="og:image" content="{{ asset_url('images/notify-og-image.png') }}">
|
||||
{% endblock %}
|
||||
<script type="text/javascript" src="{{ asset_url('js/gtm_head.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block pageTitle %}
|
||||
{% block per_page_title %}{% endblock %} – Notify.gov
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyStart %}
|
||||
{% block extra_javascripts_before_body %}
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.platform_admin %}
|
||||
{% set navigation = [
|
||||
{
|
||||
"href": url_for("main.show_accounts_or_dashboard"),
|
||||
"text": "Current service",
|
||||
"active": header_navigation.is_selected('accounts-or-dashboard')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.get_started'),
|
||||
"text": "Using Notify",
|
||||
"active": header_navigation.is_selected('using_notify')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.features'),
|
||||
"text": "Features",
|
||||
"active": header_navigation.is_selected('features')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.platform_admin_splash_page'),
|
||||
"text": "Platform admin",
|
||||
"active": header_navigation.is_selected('platform-admin')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.support'),
|
||||
"text": "Contact us",
|
||||
"active": header_navigation.is_selected('support')
|
||||
}
|
||||
] %}
|
||||
{% if current_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 navigation = [
|
||||
{
|
||||
"href": url_for("main.show_accounts_or_dashboard"),
|
||||
"text": "Current service",
|
||||
"active": header_navigation.is_selected('accounts-or-dashboard')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.get_started'),
|
||||
"text": "Using Notify",
|
||||
"active": header_navigation.is_selected('using_notify')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.features'),
|
||||
"text": "Features",
|
||||
"active": header_navigation.is_selected('features')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.support'),
|
||||
"text": "Contact us",
|
||||
"active": header_navigation.is_selected('support')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.user_profile'),
|
||||
"text": "User profile",
|
||||
"active": header_navigation.is_selected('user-profile')
|
||||
}
|
||||
] %}
|
||||
{% if current_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 %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- Add navigation back after pilot -->
|
||||
{# {% set navigation = [
|
||||
{
|
||||
"href": url_for('main.get_started'),
|
||||
"text": "Using Notify",
|
||||
"active": header_navigation.is_selected('using_notify')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.features'),
|
||||
"text": "Features",
|
||||
"active": header_navigation.is_selected('features')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.support'),
|
||||
"text": "Contact us",
|
||||
"active": header_navigation.is_selected('support')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_in'),
|
||||
"text": "Sign in",
|
||||
"active": header_navigation.is_selected('sign-in')
|
||||
}
|
||||
] %} #}
|
||||
{% endif %}
|
||||
|
||||
{{ usaHeader({
|
||||
"homepageUrl": url_for('main.show_accounts_or_dashboard'),
|
||||
"productName": "Notify",
|
||||
"navigation": navigation,
|
||||
"navigationClasses": "govuk-header__navigation--end",
|
||||
"secondaryNavigation": secondaryNavigation,
|
||||
"assetsPath": asset_path + "images"
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<!-- lines 179 - 251 can all be deleted and replaced with {% include "new/components/footer.html" %} -->
|
||||
{% if current_service and current_service.research_mode %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a><span id="research-mode" class="research-mode">research mode</span>' %}
|
||||
{% else %}
|
||||
{% set commit_hash = ", Latest version: " + config['COMMIT_HASH'] %}
|
||||
{% set long_link = '<a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||
{% endif %}
|
||||
|
||||
{{ usaFooter({
|
||||
"classes": "js-footer",
|
||||
"navigation": [
|
||||
{
|
||||
"title": "About Notify",
|
||||
"columns": 1,
|
||||
"items": [
|
||||
{
|
||||
"href": url_for("main.features"),
|
||||
"text": "Features"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.roadmap"),
|
||||
"text": "Roadmap"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.security"),
|
||||
"text": "Security"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Using Notify",
|
||||
"columns": 1,
|
||||
"items": [
|
||||
{
|
||||
"href": url_for("main.get_started"),
|
||||
"text": "Get started"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.pricing"),
|
||||
"text": "Pricing"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.trial_mode_new"),
|
||||
"text": "Trial mode"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.message_status"),
|
||||
"text": "Delivery status"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.guidance_index"),
|
||||
"text": "Guidance"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.documentation"),
|
||||
"text": "API documentation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Support",
|
||||
"columns": 1,
|
||||
"items": [
|
||||
{
|
||||
"href": url_for('main.support'),
|
||||
"text": "Contact us"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
"meta": {
|
||||
"items": meta_items,
|
||||
"html": meta_suffix
|
||||
}
|
||||
}) }}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% block sessionUserWarning %}
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timerWarning">
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" 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>
|
||||
<div class="usa-prose">
|
||||
<p>You have been inactive for too long.
|
||||
Your session will expire in <span id="timeLeft" role="timer"></span>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button type="button" class="usa-button" id="extendSessionTimer" data-close-modal>
|
||||
Extend Session
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button type="button" class="usa-button usa-button--unstyled padding-105 text-center" id="logOutTimer"
|
||||
data-close-modal>
|
||||
Sign out
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block bodyEnd %}
|
||||
{% block extra_javascripts %}
|
||||
|
||||
{% endblock %}
|
||||
<!--[if gt IE 8]><!-->
|
||||
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
|
||||
<!--<![endif]-->
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/sub-navigation.html" import sub_navigation %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{ content_page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="grid-row">
|
||||
|
||||
{% if navigation_links %}
|
||||
<div class="tablet:grid-col-2 margin-bottom-4">
|
||||
{{ sub_navigation(navigation_links) }}
|
||||
</div>
|
||||
<div class="tablet:grid-col-10 tablet:padding-left-4 usa-prose site-prose">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-10">
|
||||
{% endif %}
|
||||
|
||||
{% block content_column_content %}
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,32 +0,0 @@
|
||||
{# This template is an old version #}
|
||||
{% if help %}
|
||||
{% include 'partials/tour.html' %}
|
||||
{% else %}
|
||||
<nav class="nav margin-bottom-4">
|
||||
<a class="usa-button margin-top-1 margin-bottom-5 width-full"
|
||||
href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a>
|
||||
<ul class="usa-sidenav">
|
||||
{% if current_user.has_permissions() %}
|
||||
{% if current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
|
||||
{% endif %}
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('templates') }}" href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a></li> #}
|
||||
{% if not current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
|
||||
{% endif %}
|
||||
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||
{% endif %} -->
|
||||
{% if current_user.has_permissions('manage_api_keys') %}
|
||||
<!-- <li><a class="usa-link{{ main_navigation.is_selected('api-integration') }}" href="{{ url_for('.api_integration', service_id=current_service.id) }}">API integration</a></li> -->
|
||||
{% endif %}
|
||||
{% elif current_user.has_permissions(allow_org_user=True) %}
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@@ -1,70 +0,0 @@
|
||||
{% from "./components/components/skip-link/macro.njk" import usaSkipLink -%}
|
||||
{% from "./components/components/header/macro.njk" import usaHeader -%}
|
||||
{% from "./components/components/footer/macro.njk" import usaFooter -%}
|
||||
{# specify absolute url for the static assets folder e.g. http://wwww.domain.com/assets #}
|
||||
{%- set assetUrl = assetUrl | default(assetPath) -%}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ htmlLang | default('en') }}" class="{{ htmlClasses }}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{% block pageTitle %}Notify.gov{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="f0f0f0" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="1b1b1b" />
|
||||
{% if config['NR_MONITOR_ON'] %}
|
||||
{% include "partials/newrelic.html" -%}
|
||||
{% endif %}
|
||||
|
||||
{# Ensure that older IE versions always render with the correct rendering engine #}
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
{% block headIcons %}
|
||||
<link rel="shortcut icon" href="{{ assetPath | default('/static') }}/images/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32"href="{{ assetPath | default('/static') }}/images/favicon-32x32.png"/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/static') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/static') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/static') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<link href="{{ assetPath | default('/static') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
{# The default og:image is added below head so that scrapers see any custom metatags first, and this is just a fallback #}
|
||||
{% block meta %}
|
||||
<meta property="og:site_name" content="Notify.gov">
|
||||
<meta property="og:image" content="{{ asset_url('images/notify-og-image.png') }}">
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||
{% block bodyStart %}{% endblock %}
|
||||
|
||||
{% block skipLink %}
|
||||
{{ usaSkipLink({
|
||||
"href": '#main-content',
|
||||
"text": 'Skip to main content'
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{{ usaHeader({}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
{% block beforeContent %}{% endblock %}
|
||||
<main class="{{ mainClasses }}" id="main-content" role="main">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
{{ usaFooter({}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyEnd %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
{# This template is an old version #}
|
||||
<nav class="navigation">
|
||||
<ul>
|
||||
<li><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Usage</a></li>
|
||||
<li><a class="usa-link{{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
|
||||
{% if current_user.platform_admin %}
|
||||
<li><a class="usa-link{{ org_navigation.is_selected('settings') }}" href="{{ url_for('.organization_settings', org_id=current_org.id) }}">Settings</a></li>
|
||||
<li><a class="usa-link{{ org_navigation.is_selected('trial-services') }}" href="{{ url_for('.organization_trial_mode_services', org_id=current_org.id) }}">Trial mode services</a></li>
|
||||
<li><a class="usa-link{{ org_navigation.is_selected('billing') }}" href="{{ url_for('.organization_billing', org_id=current_org.id) }}">Billing</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -1,35 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{% block org_page_title %}{% endblock %} – {{ current_org.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
<div class="navigation-service usa-breadcrumb">
|
||||
{% if current_user.platform_admin %}
|
||||
<a href="{{ url_for('.organizations') }}" class="usa-link navigation-organization-link">Organizations</a>
|
||||
{% endif %}
|
||||
<div class="navigation-service">
|
||||
{{ current_org.name }}
|
||||
</div>
|
||||
<a href="{{ url_for('main.choose_account') }}" class="usa-link navigation-service">Switch service</a>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-3">
|
||||
{% include "org_nav.html" %}
|
||||
</div>
|
||||
<div class="grid-col-9">
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
<main class="main" id="main-content" role="main" >
|
||||
{% block content %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% block maincolumn_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,20 +0,0 @@
|
||||
{# This template is an old version #}
|
||||
{% macro navigation_service_name(service) %}
|
||||
<div class="font-body-2xl text-bold">
|
||||
{{ service.name }}
|
||||
{% if not service.active %}
|
||||
<span class="navigation-service-name navigation-service-type--suspended">Suspended</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="navigation-service margin-top-5 display-flex flex-align-end flex-justify border-bottom padding-bottom-1">
|
||||
{% if current_service.organization_id %}
|
||||
{% if current_user.platform_admin or
|
||||
(current_user.belongs_to_organization(current_service.organization_id) and current_service.live) %}
|
||||
<a href="{{ url_for('.organization_dashboard', org_id=current_service.organization_id) }}" class="usa-link navigation-organization-link">{{ current_service.organization_name }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ navigation_service_name(current_service) }}
|
||||
<a href="{{ url_for('main.choose_account') }}" class="usa-link">Switch service</a>
|
||||
</div>
|
||||
@@ -1,17 +0,0 @@
|
||||
{# This template is an old version #}
|
||||
{% if help %}
|
||||
{% include 'partials/tour.html' %}
|
||||
{% else %}
|
||||
<nav class="nav">
|
||||
<ul class="usa-sidenav">
|
||||
{# {% if current_user.has_permissions() %} #}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}"
|
||||
href="{{ url_for('main.service_settings', service_id=current_service.id) }}">General</a></li>
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('user-profile') }}"
|
||||
href="{{ url_for('main.user_profile', service_id=current_service.id) }}">User profile</a></li>
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('team-members') }}"
|
||||
href="{{ url_for('main.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
{# {% endif %} #}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@@ -1,35 +0,0 @@
|
||||
{# This template is an old version #}
|
||||
{% extends "admin_template.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{% block service_page_title %}{% endblock %} – {{ current_service.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-row margin-top-5">
|
||||
{% if help %}
|
||||
<div class="tablet:grid-col-3">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-3 margin-bottom-4">
|
||||
{% endif %}
|
||||
{% include "settings_nav.html" %}
|
||||
</div>
|
||||
{% if help %}
|
||||
<div class="tablet:grid-col-8">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-9 tablet:padding-left-4">
|
||||
{% endif %}
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
<main id="main-content" role="main" class="usa-prose site-prose margin-bottom-10">
|
||||
{% block content %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% block maincolumn_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,36 +0,0 @@
|
||||
{# This template is an old version #}
|
||||
{% extends "admin_template.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{% block service_page_title %}{% endblock %} – {{ current_service.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
{% include "service_navigation.html" %}
|
||||
<div class="grid-row margin-top-5">
|
||||
{% if help %}
|
||||
<div class="tablet:grid-col-3">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-3">
|
||||
{% endif %}
|
||||
{% include "main_nav.html" %}
|
||||
</div>
|
||||
{% if help %}
|
||||
<div class="grid-col-8">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-9 tablet:padding-left-4">
|
||||
{% endif %}
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
<main id="main-content" role="main" class="usa-prose site-prose margin-bottom-10">
|
||||
{% block content %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% block maincolumn_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set mainClasses = "margin-top-5 padding-bottom-5" %}
|
||||
|
||||
{% block beforeContent %}
|
||||
{% if current_service and current_service.active and current_user.is_authenticated and current_user.belongs_to_service(current_service.id) %}
|
||||
<!-- <div class="navigation-service usa-breadcrumb">
|
||||
<a href="{{ url_for('main.show_accounts_or_dashboard') }}" class="usa-link usa-breadcrumb__link usa-breadcrumb-back-to">Back to {{ current_service.name }}</a>
|
||||
</div> -->
|
||||
{% endif %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% block maincolumn_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -25,7 +25,7 @@
|
||||
{% if job.scheduled_for %}
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Your text has been scheduled</h4>
|
||||
<h2 class="usa-alert__heading">Your text has been scheduled</h2>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
||||
</p>
|
||||
@@ -35,7 +35,7 @@
|
||||
{% else %}
|
||||
<div class="usa-alert usa-alert--success">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Your text has been sent</h4>
|
||||
<h2 class="usa-alert__heading">Your text has been sent</h2>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }} was sent on {% if job.processing_started %}
|
||||
{{ job.processing_started|format_datetime_table }} {% else %}
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if page == 3 and pagination.last > 4 %}
|
||||
<li class="usa-pagination__item usa-pagination__overflow" aria-label="ellipsis indicating non-visible pages">
|
||||
<span>…</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if next_page %}
|
||||
@@ -58,7 +53,6 @@
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endset %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
<div class="margin-bottom-8">
|
||||
<h1 class="usa-sr-only">All activity</h1>
|
||||
@@ -66,7 +60,7 @@
|
||||
<h2 class="margin-top-4 margin-bottom-1">Sent jobs</h2>
|
||||
<div class="usa-table-container--scrollable-mobile">
|
||||
<table class="usa-table usa-table--compact job-table">
|
||||
<caption></caption>
|
||||
<caption class="usa-sr-only">Table showing all sent jobs for this service</caption>
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId">
|
||||
@@ -96,7 +90,7 @@
|
||||
{% if all_jobs_dict %}
|
||||
{% for job in all_jobs_dict %}
|
||||
<tr class="table-row">
|
||||
<td class="table-field jobid" scope="row" role="rowheader">
|
||||
<td class="table-field jobid" role="rowheader">
|
||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||
</a>
|
||||
@@ -109,8 +103,11 @@
|
||||
<td class="table-field sender">{{ job.created_by.name }}</td>
|
||||
<td class="table-field report">
|
||||
{% if job.time_left != "Data no longer available" %}
|
||||
<a href="{{ job.download_link }}"><img src="{{ url_for('static', filename='img/material-icons/file_download.svg') }}" alt="File Download Icon"></a>
|
||||
{% elif job %}
|
||||
<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>
|
||||
</a>
|
||||
{% else %}
|
||||
<span>N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -120,9 +117,9 @@
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr class="table-row">
|
||||
<td class="table-empty-message" colspan="10">No batched job messages found (messages are kept for {{ service_data_retention_days }} days).</td>
|
||||
<td class="table-empty-message" colspan="10">No messages found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="usa-sr-only usa-table__announcement-region" aria-live="polite"></div>
|
||||
|
||||
145
app/templates/views/best-practices/benchmark-performance.html
Normal file
@@ -0,0 +1,145 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set page_title = "Measuring performance with benchmarking" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{page_title}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p class="font-sans-lg text-base">Learn how effective your texting program can be.</p>
|
||||
|
||||
<p>
|
||||
Several factors determine a messaging campaign’s success, but the performance of previous projects can help set
|
||||
expectations. Get to know what other government texting initiatives have done as a way to benchmark your own
|
||||
performance.
|
||||
</p>
|
||||
<h2>What other texting studies have found</h2>
|
||||
<p>
|
||||
When the <a
|
||||
href="https://www.cbpp.org/research/food-assistance/targeted-text-message-outreach-can-increase-wic-enrollment-pilots-show"
|
||||
target="_blank">Center on Budget and Policy Priorities</a> studied WIC, they found key learnings about the
|
||||
quantity of messages delivered, how people engage with messages, and how they take action.
|
||||
</p>
|
||||
|
||||
<h3 class="padding-left-5">Message delivery</h3>
|
||||
<p class="font-body-lg text-light padding-left-5">Benchmark: <span class="text-bold">80%</span> of texts are
|
||||
successfully delivered
|
||||
</p>
|
||||
<div class="padding-left-2 measure-4">
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
You may discover that some numbers are temporarily or permanently unavailable due to service being
|
||||
discontinued,
|
||||
numbers changing, or being a landline.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<h3 class="padding-left-5">Engagement</h3>
|
||||
<div class="measure-4 padding-left-5">
|
||||
<p class="font-body-lg text-light">Benchmark: Engagement rates with texts ranged from <span
|
||||
class="text-bold">17%</span>
|
||||
to <span class="text-bold">26%</span></p>
|
||||
</div>
|
||||
<div class="padding-left-2 measure-4">
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
The highest rate of engagement with a text comes within hours of sending. Engagement rates include any kind
|
||||
of
|
||||
action taken due to a text, including replying “STOP” to prevent future texts.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 class="padding-left-5">Appointment requests</h3>
|
||||
<div class="measure-4 padding-left-5">
|
||||
<p class="font-body-lg text-light">Benchmark: Requesting appointments after receiving texts ranged
|
||||
from <span class="text-bold">4%</span> to <span class="text-bold">9%</span></p>
|
||||
</div>
|
||||
<div class="padding-left-2 measure-4">
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
Requesting appointments is a specific type of engagement. Provide a phone number or link to an online
|
||||
appointment
|
||||
request form.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The <a href="https://codeforamerica.org/resources/texting-playbook/" target="_blank">Code for America’s Texting
|
||||
Playbook</a>
|
||||
reported specific learnings around appointment reminders, completing
|
||||
document submission, and maintenance reminders.
|
||||
</p>
|
||||
<h3 class="padding-left-5">Appointment reminders</h3>
|
||||
<div class="measure-4 padding-left-5">
|
||||
<p class="font-body-lg text-light">Benchmark: Clients were <span class="text-bold">79%</span> more
|
||||
likely
|
||||
to keep
|
||||
their appointment after receiving a text reminder.</p>
|
||||
</div>
|
||||
<div class="padding-left-2 measure-4">
|
||||
<ul>
|
||||
<li>
|
||||
<p>You will likely see more completed appointments.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="measure-4 padding-left-5">
|
||||
<p class="font-body-lg text-light">Benchmark: Clients were <span class="text-bold">55%</span> more
|
||||
likely
|
||||
to complete
|
||||
an interview after receiving an interview reminder</p>
|
||||
</div>
|
||||
<div class="padding-left-2 measure-4">
|
||||
<ul>
|
||||
<li>
|
||||
<p>You will likely see more completed interviews.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 class="padding-left-5">Document submission</h3>
|
||||
<div class="measure-4 padding-left-5">
|
||||
<p class="font-body-lg text-light">
|
||||
Benchmark: Clients were <span class="text-bold">6%</span> more likely to complete document submission after
|
||||
receiving a customized list of required documents via text
|
||||
</p>
|
||||
</div>
|
||||
<div class="padding-left-2 measure-4">
|
||||
<ul>
|
||||
<li>
|
||||
<p>To encourage response, provide a custom list of the needed documents and information about how to submit
|
||||
them.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 class="padding-left-5">Reminders</h3>
|
||||
<div class="measure-4 padding-left-5">
|
||||
<p class="font-body-lg text-light">Benchmark: Text reminders improved case maintenance rates by <span
|
||||
class="text-bold">21%</span></p>
|
||||
</div>
|
||||
<div class="padding-left-2 measure-4">
|
||||
<ul>
|
||||
<li>
|
||||
<p>You may see less turnover in your case rates.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
64
app/templates/views/best-practices/best-practices.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Best Practices
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
|
||||
<section class="usa-prose">
|
||||
<h1>Best Practices</h1>
|
||||
<p class="font-sans-lg text-base">For texting the public</p>
|
||||
<p>Effectively reaching your audience and supporting your program’s goals starts with strategically planning out what
|
||||
text messages can help you achieve and how to approach a thoughtful rollout.
|
||||
</p>
|
||||
|
||||
<p>This set of best practices will help you get an effective texting initiative up and running.</p>
|
||||
<h2 class="padding-bottom-2">
|
||||
Key elements of a texting campaign
|
||||
</h2>
|
||||
|
||||
{% set card_contents = [
|
||||
{
|
||||
"svg_src": "goal",
|
||||
"card_heading": "Establish clear goals",
|
||||
"p_text": "Start with a singular purpose. Make explicit what you want to achieve.",
|
||||
"link": "/best-practices/clear-goals"
|
||||
},
|
||||
{
|
||||
"svg_src": "compliant",
|
||||
"card_heading": "Follow rules & regulations",
|
||||
"p_text": "Understand what is required when texting the public.",
|
||||
"link": "/best-practices/rules-and-regulations"
|
||||
},
|
||||
{
|
||||
"svg_src": "trust",
|
||||
"card_heading": "Establish trust",
|
||||
"p_text": "Help your audience anticipate and welcome your texts.",
|
||||
"link": "/best-practices/establish-trust"
|
||||
},
|
||||
{
|
||||
"svg_src": "runner",
|
||||
"card_heading": "Write texts that provoke action",
|
||||
"p_text": "Help your audience know what to do with the information you send.",
|
||||
"link": "/best-practices/write-for-action"
|
||||
},
|
||||
{
|
||||
"svg_src": "language",
|
||||
"card_heading": "Send texts in multiple languages",
|
||||
"p_text": "What to know as you plan translated texts.",
|
||||
"link": "/best-practices/multiple-languages"
|
||||
},
|
||||
{
|
||||
"svg_src": "chart",
|
||||
"card_heading": "Measure performance with benchmarking",
|
||||
"p_text": "Learn how effective your texting program can be.",
|
||||
"link": "/best-practices/benchmark-performance"
|
||||
}
|
||||
] %}
|
||||
|
||||
{% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{%
|
||||
endwith %}
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
201
app/templates/views/best-practices/clear-goals.html
Normal file
@@ -0,0 +1,201 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set page_title = "Establish clear goals" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{page_title}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p class="font-sans-lg text-base">Start with a singular purpose. Make explicit what you want to achieve.</p>
|
||||
<div class="grid-row grid-gap-3">
|
||||
<div class="tablet:grid-col">
|
||||
<p>Text messaging should be one part of how you communicate with the people you serve, and it is best used to
|
||||
provoke
|
||||
action or influence behavior. Therefore, when starting to plan your texting campaign, it’s important to start
|
||||
with
|
||||
the end in mind.
|
||||
</p>
|
||||
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading" id="summary-box-establish-clear-goals">
|
||||
To establish clear goals, start by answering the following questions:
|
||||
</p>
|
||||
<div class="usa-summary-box__text">
|
||||
<ul class="usa-list">
|
||||
<li><i>Who</i> do you want to text?</li>
|
||||
<li><i>At what moments</i> do you want to text them?</li>
|
||||
<li><i>Why</i> do you want to text them?</li>
|
||||
<li>What do you want them to <i>do</i>?</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tablet:grid-col width-full padding-top-3">
|
||||
<div class="grid-container">
|
||||
<div class="usa-card__container border-0 width-full overflow-hidden maxh-mobile-lg">
|
||||
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
|
||||
<div class="position-absolute flex text-center padding-5 padding-top-15">
|
||||
<p class="sms-message-wrapper text-left">Dept. of Human Services: We believe your EBT card may have been
|
||||
used by
|
||||
someone else. Please call the number on the back of your card (888-123-5555) to cancel and get a new
|
||||
one.
|
||||
If
|
||||
you
|
||||
need a new card immediately, please visit your local office.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h2>When should you send a text?</h2>
|
||||
<div class="grid-row grid-gap-3">
|
||||
<div class="tablet:grid-col">
|
||||
<div class="do-dont">
|
||||
<div class="do-dont__do">
|
||||
<h3 class="do-dont__heading"><a id="what-to-do" class="usa-anchor"></a>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{{ url_for('static', filename='img/sprite.svg') }}#check_circle"></use>
|
||||
</svg>
|
||||
What to do
|
||||
</h3>
|
||||
<div class="do-dont__content">
|
||||
<p><b>Use texting to</b> share timely
|
||||
information, provide critical status updates, or remind someone to do something.</p>
|
||||
<p class="margin-bottom-2">Examples:</p>
|
||||
|
||||
{% set list_item_dos = [
|
||||
{
|
||||
"list_content": "Your application is due on…",
|
||||
},
|
||||
{
|
||||
"list_content": "We need more information from you to process your application for…",
|
||||
},
|
||||
{
|
||||
"list_content": "We will be calling you about…at…",
|
||||
},
|
||||
{
|
||||
"list_content": "We will be calling you about…at…",
|
||||
},
|
||||
] %}
|
||||
<ul>
|
||||
{% for item in list_item_dos %}
|
||||
<li>
|
||||
{{item.list_content}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tablet:grid-col">
|
||||
<div class="do-dont">
|
||||
<div class="do-dont__dont">
|
||||
<h3 class="do-dont__heading"><a id="what-not-to-do" class="usa-anchor"></a>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{{ url_for('static', filename='img/sprite.svg') }}#cancel"></use>
|
||||
</svg> What not to do
|
||||
</h3>
|
||||
<div class="do-dont__content">
|
||||
<p>Texting is <b>not great for</b> long,
|
||||
detailed instructions or frequent reminders that may be ignored.
|
||||
</p>
|
||||
<p class="margin-bottom-2">Examples:</p>
|
||||
{% set list_item_donts = [
|
||||
{
|
||||
"list_content": "Your rights and responsibilities",
|
||||
},
|
||||
{
|
||||
"list_content": "How to complete your application",
|
||||
},
|
||||
{
|
||||
"list_content": "Weekly reminder not to share your account information",
|
||||
},
|
||||
] %}
|
||||
<ul>
|
||||
{% for item in list_item_donts %}
|
||||
<li>
|
||||
{{item.list_content}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>Your program may already be communicating with your audience through methods like mailed notices and phone calls.
|
||||
Think of text messages as complementary to your existing communications. Review how well you are currently reaching
|
||||
your audience. Are there methods or gaps that could be supplemented with a text message?
|
||||
</p>
|
||||
<p>
|
||||
The Department of Veterans Affairs provides a <a class="use-link usa-link--external"
|
||||
href="https://github.com/department-of-veterans-affairs/va.gov-team/blob/master/products/va-notify/notification-guide.md#vanotify-notification-guide"
|
||||
target="_blank">helpful flow-chart</a> that can help you decide if a text message is needed
|
||||
for the communication problem you are trying to solve.
|
||||
</p>
|
||||
<h2>Use a hypothesis framework to plan your campaign</h2>
|
||||
<p>
|
||||
Think of your texting campaign as a scientific experiment, and try to articulate what you hope to improve as a
|
||||
result of implementing text messaging at your agency. For example:
|
||||
</p>
|
||||
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
|
||||
<div class="usa-summary-box__body">
|
||||
<div class="usa-summary-box__text">
|
||||
<p class="margin-top-0 font-body-lg">
|
||||
We believe that texting <i>[specific group of people]</i> when <i>[this happens or on this
|
||||
cadence]</i> to inform/alert them about <i>[subject]</i> will result in <i>[desired action]</i>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Review your drafted hypothesis with your team to make sure everyone is aligned on your desired goals. A clear and
|
||||
concise hypothesis can help you decide how to <a href="../best-practices/write-for-action">write text message
|
||||
content
|
||||
that provokes action</a>.
|
||||
</p>
|
||||
<h2>Begin with a small test</h2>
|
||||
<p>
|
||||
To help you decide how much to invest in text messaging over time, we highly recommend starting small and testing
|
||||
your message campaigns to refine based on feedback and evidence.
|
||||
</p>
|
||||
<p>
|
||||
Starting small has less risk and more flexibility. It can help you gather data and decide <i>if and how</i> to send
|
||||
more text messages. For example, start by sending one message to a smaller sample of your audience and track their
|
||||
resulting behavior.
|
||||
</p>
|
||||
<p>
|
||||
At a minimum, make sure to get feedback on your message content from staff, community-based organizations, and,
|
||||
ideally, from people who receive the texts themselves to ensure your messages are clear and actionable.
|
||||
</p>
|
||||
<h2>Measure performance</h2>
|
||||
<p>Building and operationalizing a texting initiative requires time and effort, and you want to be able to demonstrate
|
||||
the results of that effort.</p>
|
||||
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
|
||||
<div class="usa-summary-box__body">
|
||||
<div class="usa-summary-box__text">
|
||||
<p class="margin-top-0">
|
||||
To understand if your texting program is successful, ask:
|
||||
</p>
|
||||
<ul class="usa-list">
|
||||
<li>What do you want to be different for your program or your desired audience after a text message is sent,
|
||||
as compared to your current state?</li>
|
||||
<li>How will you know if text recipients take the desired action?</li>
|
||||
<li>Do you have access to existing data in your system that you could use to determine whether texting is
|
||||
giving you the results you want?</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
193
app/templates/views/best-practices/establish-trust.html
Normal file
@@ -0,0 +1,193 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/best-practices/circle_number.html" import circle_number %}
|
||||
|
||||
{% set page_title = "Establish trust" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{page_title}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p class="font-sans-lg text-base">Help your audience anticipate and welcome your texts.</p>
|
||||
<p>
|
||||
People are wary of texts they’re not expecting to receive. Before you send your first text, consider how you will
|
||||
gain trust with your audience and address any concerns of spam. Seize specific moments to reinforce what your
|
||||
audience should expect from your messages.
|
||||
</p>
|
||||
<h2 id="get-the-word-out">Get the word out</h2>
|
||||
<p>Before you launch, let your audience know you are adding texting as a communication method. People are more likely
|
||||
to trust your text once they get it if they know that your program is sending texts.</p>
|
||||
|
||||
<p>How you currently communicate with your audience will determine the best approaches to announce your new
|
||||
initiative. Harness channels you’re already using like social media, public websites, mailing inserts, or fliers at
|
||||
key service locations as well as partnering with community organizations to get the word out about your new texting
|
||||
program.</p>
|
||||
<h3>An example of key messages in direct mail</h3>
|
||||
<figure>
|
||||
<img src="{{asset_url('images/snap-renewal.jpg')}}" alt="snap-renewal" width="100%">
|
||||
<figcaption>A paper mailer sent out by the State of Connecticut, in partnership with Code for America, to get the
|
||||
word out about their new SNAP texting initiative. Flier displays an image of an introduction text and includes key
|
||||
messages to reinforce legitimacy.</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3>Prime your audiences to trust you</h3>
|
||||
<ul class="usa-card-group flex-1 flex-wrap padding-top-2">
|
||||
<li class="flex-1 width-full height-full padding-bottom-2">
|
||||
<div class="usa-card__container border-0">
|
||||
<p>To reinforce legitimacy, include these key messages in your outreach:</p>
|
||||
<ol class="best-practices-list">
|
||||
<li><b>Introduce yourself</b> – Give people time to get to know your texting communications before you need
|
||||
direct action from them</li>
|
||||
<li><b>Phone number</b> – Note the sending phone number your texts will come from</li>
|
||||
<li><b>Text types</b> – Be clear on the types of texts you intend to send, how often, and why they matter</li>
|
||||
<li><b>Privacy policy</b>– For example, state that you will not collect personal information, including the
|
||||
types of information you will never ask for</li>
|
||||
<li><b>Places to verify</b> – Include a phone number, website, or human where people can verify that the texts
|
||||
are coming from you</li>
|
||||
<li><b>Opt out</b> – Provide opt-out instructions and a disclaimer for the potential cost of receiving the
|
||||
text
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card flex-1 usa-card--media-right width-full padding-top-3">
|
||||
<div class="usa-card__container border-0 width-full overflow-hidden height-phone-lg">
|
||||
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
|
||||
<div class="position-absolute flex text-center padding-5 padding-top-15">
|
||||
<p class="sms-message-wrapper text-left">Dept. of Social Services: Welcome to text updates from the
|
||||
{{circle_number(1)}}<b>Department of Social Services.</b> {{ circle_number(2) }}<b>We’ll always text
|
||||
you from
|
||||
this number,</b> save us to your contact list. Our goal is to {{ circle_number(3) }} <b>keep you up to
|
||||
date on
|
||||
your account status.</b> {{ circle_number(4) }} <b>We will never ask for personal details in a text.</b>
|
||||
{{ circle_number(5) }} If you have questions about how we protect your privacy, see
|
||||
state.gov/privacy.{{circle_number(6) }}<b>Reply STOP to stop.</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="as-people-receive-texts">As people receive texts</h2>
|
||||
<p>When a person has questions or needs assistance, reaching out for support may be natural for them. Give direction
|
||||
in your texts so your audience understands what to do with the information you provide. Remember, it’s important to
|
||||
repeat consistent and standardized language across texts and programs.</p>
|
||||
<h3>Be trustworthy</h3>
|
||||
<ul class="usa-card-group flex-1 flex-wrap padding-top-2">
|
||||
<li class="flex-1 width-full height-full padding-bottom-2">
|
||||
<div class="usa-card__container border-0">
|
||||
<ol start="7" class="best-practices-list set-two">
|
||||
<li>Identify yourself in the text message. Use a consistent and familiar <b>program name</b> to precede each
|
||||
text you send.</li>
|
||||
<li>Personalize with <b>first name</b>. Through A/B testing, Code for America saw an increase in
|
||||
follow-through by 25% if someone’s first name was included.</li>
|
||||
<li>Use <b>full and direct .gov website URLs</b>. Avoid link shorteners, as they obscure your links and
|
||||
potentially make it easier for a malicious actor to spoof your text.</li>
|
||||
<li>If providing a phone number, use a familiar phone number and <b>provide information on how to verify</b>
|
||||
its legitimacy.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card flex-1 usa-card--media-right width-full padding-top-1">
|
||||
<div class="usa-card__container border-0 width-full height-mobile overflow-hidden">
|
||||
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
|
||||
<div class="position-absolute flex text-center padding-5 padding-top-10">
|
||||
<p class="sms-message-wrapper text-left">
|
||||
{{circle_number(7) }}<b>Dept. of Social Services:</b> Hi {{circle_number(8) }}<b>Julie,</b> Your Medicaid
|
||||
renewal is closing December 31, 2023. You can renew online at {{circle_number(9) }}<a
|
||||
href="https://www.application.yourstate.gov" target="_blank"
|
||||
rel="application.yourstate.gov">https://www.application.yourstate.gov</a> or {{circle_number(10) }}
|
||||
<b>call the number on the back</b> of your Medicaid card.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Provide helpful auto-responses</h3>
|
||||
<ul class="usa-card-group flex-1 flex-wrap padding-top-2">
|
||||
<li class="flex-1 width-full height-full padding-bottom-2">
|
||||
<div class="usa-card__container border-0">
|
||||
<p>If you're sending one-way notifications, phone carriers allow a single auto-response message that will be
|
||||
generated if a recipient tries to text a response to your message. Use the auto-response to reaffirm your key
|
||||
messages around legitimacy and communicate to recipients that texts are coming from an automated system.</p>
|
||||
<ol class="best-practices-list set-three">
|
||||
<li>Provide <b>a way to contact a human</b> in any auto-response text</li>
|
||||
<li>Provide <b>information on how to opt out</b> and opt back in</li>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card flex-1 usa-card--media-right width-full padding-top-3">
|
||||
<div class="usa-card__container border-0 width-full overflow-hidden best-practices">
|
||||
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
|
||||
<div class="position-absolute flex text-center padding-5 padding-top-15">
|
||||
<p class="sms-message-wrapper text-left">
|
||||
Dept. of Social Services: This number is unmonitored. To {{circle_number(11) }} <b>contact us, call
|
||||
888-123-4567</b>. We will never ask for personal details in a text. If you have questions about how to
|
||||
protect your privacy, see state.gov/privacy {{circle_number(12) }} <b>Reply “STOP”</b> to this phone number
|
||||
to opt out of receiving texts, and “START” to opt back in.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Be prepared for questions</h2>
|
||||
<p>
|
||||
Your texting program will only be perceived as legitimate if you get the word out in many different ways. It's also
|
||||
important that agency employees who support text recipients (for example, the call center) are familiar with the
|
||||
texting program. They can accurately respond to recipient questions about them, so make sure you’re priming your
|
||||
delivery staff, especially those who interact directly with your intended audience.
|
||||
</p>
|
||||
|
||||
<h3 class="padding-bottom-2">Harness trusted communication channels</h3>
|
||||
{% set card_contents = [
|
||||
{
|
||||
"image_src": asset_url('images/website.svg'),
|
||||
"card_heading": "Websites",
|
||||
"alt_text": "Graphic representing a website browser",
|
||||
"p_text": "Post banner alerts about texting on your agency website and link to a press release for more information.
|
||||
Publish a public texting privacy policy.",
|
||||
},
|
||||
{
|
||||
"image_src": asset_url('images/community-org.svg'),
|
||||
"card_heading": "Community organizations",
|
||||
"alt_text": "Illustration of community organizations spreading awareness",
|
||||
"p_text": "Alert trusted community-based organizations that texts are coming and what they look like. Equip them with
|
||||
fliers, posters, and talking points to spread the word.",
|
||||
},
|
||||
{
|
||||
"image_src": asset_url('images/call-center.svg'),
|
||||
"card_heading": "IVR / Call Centers",
|
||||
"alt_text": "Illustration of a call center promoting texting initiative",
|
||||
"p_text": "Consider adding hold message announcements about your texting initiative to further get the word out.",
|
||||
},
|
||||
] %}
|
||||
|
||||
{% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{%
|
||||
endwith %}
|
||||
<h3 class="padding-bottom-2">Prepare your team</h3>
|
||||
{% set card_contents = [
|
||||
{
|
||||
"image_src": asset_url('images/grouping.svg'),
|
||||
"card_heading": "Staff",
|
||||
"alt_text": "Illustration of staff group briefing",
|
||||
"p_text": "Give all staff a heads-up about texts before and when they are sent. Share talking points on what texts
|
||||
you’re sending, when you’ll start to send them, the phone number they are coming from, and where staff can escalate
|
||||
any issues.",
|
||||
},
|
||||
{
|
||||
"image_src": asset_url('images/project-management.svg'),
|
||||
"card_heading": "Case management systems",
|
||||
"alt_text": "Graphic representing project management",
|
||||
"p_text": "When it makes sense, include information about texts being sent to specific clients on individual splash
|
||||
pages or within case management notes.",
|
||||
},
|
||||
] %}
|
||||
{% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{%
|
||||
endwith %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
30
app/templates/views/best-practices/multiple-languages.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set page_title = "Text in multiple languages" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{page_title}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p class="font-sans-lg text-base"> What to know as you plan translated texts.</p>
|
||||
|
||||
<p>
|
||||
Sending messages in recipients’ preferred language demonstrates respect, makes it more likely that recipients will
|
||||
understand and take action on the message, and may be required by your agency or program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To send translated messages using Notify.gov you will need two things: a record of your recipients’ preferred
|
||||
language and translation/s of the message template. Notify.gov does not provide translation services.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
As with messages in English, we strongly recommend user testing the translations with people who speak the language
|
||||
to ensure the messages are understandable.
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
126
app/templates/views/best-practices/rules-and-regulations.html
Normal file
@@ -0,0 +1,126 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set page_title = "Rules and regulations" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{page_title}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p class="font-sans-lg text-base">Understand what is required when texting the public.</p>
|
||||
<h2>What to know about consent and opting out</h2>
|
||||
<p>
|
||||
Providing consent and opting out are key ways for text recipients to express their text communication preferences.
|
||||
Discuss your obligation to obtain consent with your legal counsel.
|
||||
</p>
|
||||
<p>
|
||||
If you do need expressed consent, consider including a pre-checked plain language opt-in (i.e. “It’s OK to text
|
||||
me.”) on
|
||||
digital forms. Be sure to ask for an up-to-date phone number and include a question about the recipient’s preferred
|
||||
language for text messages if you expect to <a href="../best-practices/multiple-languages">translate your text
|
||||
messages</a> in
|
||||
languages other than English.
|
||||
</p>
|
||||
{% set links = [
|
||||
{
|
||||
"p_text": 'The <a class="use-link usa-link--external" href="https://www.fcc.gov/sites/default/files/tcpa-rules.pdf"
|
||||
target="_blank">Telephone Consumer
|
||||
Protection Act (TCPA)</a> (47 USC § 227) is the federal law that impacts how organizations are allowed to
|
||||
communicate
|
||||
in bulk with the public via telephone (including text message or SMS).'
|
||||
},
|
||||
{
|
||||
"p_text": 'The <a class="use-link usa-link--external"
|
||||
href="https://www.fcc.gov/document/fcc-provides-best-practices-enable-critical-health-care-coverage-calls"
|
||||
target="_blank">FCC has ruled</a> that <b>Federal</b> and <b>State</b> programs are exempt from the TCPA and can
|
||||
send
|
||||
text messages to the public without consent if conducting official business. Without explicit mention in the ruling,
|
||||
local governments, phone carriers, or any texting intermediaries might require it.'
|
||||
},
|
||||
{
|
||||
"url_link":
|
||||
"pdf/tcpa_overview.pdf",
|
||||
"url_text":"Download and share our overview of the TCPA with your legal counsel",
|
||||
"is_downloadable": true,
|
||||
"p_text": 'It provides a baseline
|
||||
interpretation to aid your legal counsel in getting up to speed with what might be needed for your program or use
|
||||
case.
|
||||
For additional questions about the enforcement of the TCPA, you can watch <a class="use-link usa-link--external"
|
||||
href="https://www.performance.gov/cx/life-experiences/facing-a-financial-shock/outputs/fcc-training/"
|
||||
target="_blank">a recorded training on public
|
||||
benefits texting</a> provided by the FCC.'
|
||||
},
|
||||
] %}
|
||||
<h3>Complying with the Telephone Consumer Protection Act (TCPA)</h3>
|
||||
<ul class="usa-collection">
|
||||
{% for item in links %}
|
||||
<li class="usa-collection__item">
|
||||
<div class="usa-collection__body">
|
||||
{% if item.url_text %}
|
||||
<h4 class="usa-collection__heading">
|
||||
<a class="usa-link {% if item.is_downloadable %}usa-link--downloadable{% else %}usa-link--external{% endif %}"
|
||||
href="{% if item.is_downloadable %}{{ asset_url(item.url_link) }}{% else %}{{ item.url_link }}{% endif %}"
|
||||
target="_blank">
|
||||
{{ item.url_text }}
|
||||
</a>
|
||||
</h4>
|
||||
{% endif %}
|
||||
<p class="usa-collection__description">
|
||||
{{item.p_text |safe}}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
<h3>Opting out</h3>
|
||||
<p>
|
||||
There is no policy requirement for senders to communicate opt-out options, but <a
|
||||
href="../best-practices/establish-trust#as-people-receive-texts"> including instructions in introductory and/or
|
||||
auto-response texts </a> on how to opt out and opt back in are effective ways to establish trust with your
|
||||
audience.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In the US, no matter where texts are coming from, a recipient has the right to revoke consent at any time through
|
||||
any
|
||||
reasonable means. For example, a text recipient can always opt out of receiving text messages from a phone number by
|
||||
responding “STOP”, “QUIT”, or <a class="usa-link usa-link--external"
|
||||
href="https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-sms-limitations-opt-out.html#settings-sms-self-opt-out-terms"
|
||||
target="_blank">one of several other keywords</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Any subsequent messages sent to that number will be blocked by the phone carrier and not delivered. We recommend
|
||||
establishing an internal process to obtain and remove numbers that fail multiple times to avoid continuing to send
|
||||
messages to them.
|
||||
</p>
|
||||
<h2>Honoring privacy</h2>
|
||||
<h3>Phone numbers are personally identifiable information (PII)</h3>
|
||||
<p>
|
||||
When putting together the data spreadsheet (or CSV) file containing text message data, follow relevant security
|
||||
processes for storing and transmitting the file that contains PII.
|
||||
</p>
|
||||
<h3>Don’t text sensitive information</h3>
|
||||
<p>
|
||||
Sensitive information should be protected and secure. Social Security Numbers, driver’s license numbers, personal
|
||||
health information, and passwords are all sensitive PII that should not be shared via text message. Consult with
|
||||
your privacy office to understand what information you can and cannot include in a text message. Different programs
|
||||
and jurisdictions may have different rules and interpretations.
|
||||
</p>
|
||||
<h3>Assess risk</h3>
|
||||
<p>
|
||||
Consider the risks if someone who is not the intended recipient receives the text message. Establish an internal
|
||||
process
|
||||
to keep your phone number list up-to-date. The FCC offers a <a class="use-link usa-link--external"
|
||||
href="https://www.fcc.gov/reassigned-numbers-database" target="_blank"
|
||||
rel="Reassigned Numbers Database">Reassigned
|
||||
Numbers Database</a> to identify phone numbers that may have been reassigned since your agency obtained them.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
106
app/templates/views/best-practices/write-for-action.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/best-practices/circle_number.html" import circle_number %}
|
||||
|
||||
{% set page_title = "Write texts that provoke action" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{page_title}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p class="font-sans-lg text-base"> Help your audience know what to do with the information you send.</p>
|
||||
<p>
|
||||
When writing a text, focus on the person who is going to receive the message and write it for them. Use plain
|
||||
language and provide easy to understand next steps.
|
||||
</p>
|
||||
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading" id="summary-box-establish-clear-goals">
|
||||
When deciding what types of messages to draft for your campaign, think through the following questions:
|
||||
</p>
|
||||
<div class="usa-summary-box__text">
|
||||
<ul class="usa-list">
|
||||
<li>What do you want recipients to do when they receive the text?</li>
|
||||
<li>What will happen if a recipient doesn't take the action you want them to take?</li>
|
||||
<li>Will you send any follow-up texts depending on if the action was taken or not?</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Be clear and concise</h2>
|
||||
<ul>
|
||||
<li>Use simple, everyday language. Keep the text short and clear. Stay under 300 characters.</li>
|
||||
<li>
|
||||
Say only one important thing per message. Focus on the most essential instructions and information. If there are
|
||||
multiple actions you want someone to take, consider sending a series of well-timed messages.
|
||||
</li>
|
||||
<li>Use a neutral, direct, and professional tone. This works better than a friendly or overly casual tone.</li>
|
||||
</ul>
|
||||
<h2>Build conditions for action</h2>
|
||||
<ul class="usa-card-group flex-1 flex-wrap padding-top-2 best-practices-flex-container">
|
||||
<li class="flex-1 width-full height-full padding-bottom-2">
|
||||
<div class="usa-card__container border-0">
|
||||
<p>For example, getting a person to update their mailing address:</p>
|
||||
<ol class="best-practices-list">
|
||||
<li>Clearly state the information and the response you want the recipient to take.</li>
|
||||
<li>Point directly to where the action can be completed, not to more information.</li>
|
||||
<li>Make sure the action can be completed via mobile phone, like calling a person or going to a
|
||||
mobile-friendly URL.
|
||||
</li>
|
||||
<li>
|
||||
Include at least two options for a recipient to take action. Some recipients may be more comfortable talking
|
||||
directly
|
||||
to a person, while others may prefer a web-based experience.
|
||||
</li>
|
||||
<li>Consider clearly explaining any consequences if the desired action is not completed.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex-1 usa-card--media-right width-full padding-top-3">
|
||||
<div class="usa-card__container border-0 width-full">
|
||||
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
|
||||
<div class="position-absolute flex text-center padding-5 padding-top-15">
|
||||
<p class="sms-message-wrapper text-left">{{circle_number(1) }}State Medicare Agency: Hello, MEDICARE has
|
||||
attempted to
|
||||
send you an update about your claim, but the mail was returned. <br>{{circle_number(2) }}Please go to
|
||||
https://www.medicare.gov/account/login?utm_source=TEXT, {{circle_number(3) }}log in to your account, and
|
||||
click “My
|
||||
Account Information” to verify that your address is correct. If you are having difficulty logging in,
|
||||
{{circle_number(4) }}call 1-800-MEDICARE for assistance. {{circle_number(5) }}We will attempt to send the
|
||||
mail again
|
||||
in 14 days.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>What provoking action looks like</h2>
|
||||
<p class="padding-bottom-2"><a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC10002044/" target="_blank">Evidence
|
||||
shows</a> that employing
|
||||
behavioral science is an effective way to increase the
|
||||
likelihood of a recipient
|
||||
taking action upon receiving your text. Keep in mind these three foundational principles while drafting your text
|
||||
message:</p>
|
||||
{% set card_contents = [
|
||||
{
|
||||
"card_heading": "Feeling a sense of ownership can influence behavior.",
|
||||
"p_text": "Example – <b>Don't miss this chance to get free internet</b> or $30 per month off your internet bill.",
|
||||
},
|
||||
{
|
||||
"card_heading": "Individuals are strongly influenced by others’ behaviors.",
|
||||
"p_text": "Example – <b>Thousands of residents</b> already get an
|
||||
average of $113/month in money for food.",
|
||||
},
|
||||
{
|
||||
"card_heading": "Individuals perceive losses more powerfully than gains.",
|
||||
"p_text": "Example – Contact us for potentially unclaimed free
|
||||
internet services <b>that belong to you</b>.",
|
||||
},
|
||||
] %}
|
||||
{% with card_contents=card_contents, text_align='left' %}{% include "components/best-practices/cards.html" %}{%
|
||||
endwith %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
{% extends "withnav_template.html" %}
|
||||
|
||||
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading, hidden_field_heading, row_heading, notification_status_field, notification_carrier_field, notification_carrier_message_field %}
|
||||
@@ -8,6 +10,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
||||
|
||||
|
||||
<div class="dashboard margin-bottom-2">
|
||||
|
||||
@@ -29,14 +33,12 @@
|
||||
</div>
|
||||
<div id="totalMessageTable" class="margin-0"></div>
|
||||
|
||||
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">
|
||||
Activity snapshot
|
||||
</h2>
|
||||
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Recent activity</h2>
|
||||
<div id="activityChartContainer">
|
||||
<form class="usa-form">
|
||||
<label class="usa-label" for="options">Account</label>
|
||||
<select class="usa-select margin-bottom-2" name="options" id="options">
|
||||
<option value>- Select -</option>
|
||||
<option value disabled>- Select -</option>
|
||||
<option value="service" selected>{{ current_service.name }}</option>
|
||||
<option value="individual">{{ current_user.name }}</option>
|
||||
</select>
|
||||
@@ -44,7 +46,7 @@
|
||||
<div id="activityChart">
|
||||
<div class="chart-header">
|
||||
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</div>
|
||||
<div class="chart-legend" aria-label="Legend"></div>
|
||||
<div class="chart-legend" role="region" aria-label="Legend"></div>
|
||||
</div>
|
||||
<div class="chart-container" id="weeklyChart"></div>
|
||||
<table id="weeklyTable" class="usa-sr-only usa-table"></table>
|
||||
@@ -58,6 +60,7 @@
|
||||
<div id="table1" class="table-overflow-x-auto hidden">
|
||||
<h2 class="margin-top-4 margin-bottom-1">My activity</h2>
|
||||
<table class="usa-table job-table">
|
||||
<caption class="usa-sr-only">Table showing the sent jobs for {{current_user.name}}</caption>
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||
@@ -72,7 +75,7 @@
|
||||
{% if job.created_by.name == current_user.name %}
|
||||
{% set notification = job.notifications[0] %}
|
||||
<tr id="{{ job.job_id }}">
|
||||
<td class="table-field jobid" scope="row" role="rowheader">
|
||||
<td class="table-field jobid" role="rowheader">
|
||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||
</a>
|
||||
@@ -98,6 +101,7 @@
|
||||
<div id="table2" class="table-overflow-x-auto visible">
|
||||
<h2 class="margin-top-4 margin-bottom-1">Service activity</h2>
|
||||
<table class="usa-table job-table">
|
||||
<caption class="usa-sr-only">Table showing the sent jobs for this service</caption>
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||
@@ -112,7 +116,7 @@
|
||||
{% for job in jobs[:5] %}
|
||||
{% set notification = job.notifications[0] %}
|
||||
<tr id="{{ job.job_id }}">
|
||||
<td class="table-field jobid" scope="row" role="rowheader">
|
||||
<td class="table-field jobid" role="rowheader">
|
||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||
</a>
|
||||
|
||||
@@ -1,35 +1,45 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, spark_bar_field %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
{% if template_statistics|length > 0 %}
|
||||
<h2 class="margin-top-4 margin-bottom-1">Recent templates</h2>
|
||||
<div class='template-statistics-table table-overflow-x-auto'>
|
||||
{% call(item, row_number) list_table(
|
||||
template_statistics,
|
||||
caption="Messages sent by template",
|
||||
caption_visible=False,
|
||||
border_visible=True,
|
||||
empty_message='',
|
||||
field_headings=[
|
||||
'Template',
|
||||
'Messages sent'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
|
||||
{% call row_heading() %}
|
||||
<a class="usa-link template-statistics-table-template-name" href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.template_id) }}">{{ item.template_name }}</a>
|
||||
<span class="template-statistics-table-hint">
|
||||
{{ 1|message_count_label(item.template_type, suffix='template')|capitalize }}
|
||||
</span>
|
||||
{% endcall %}
|
||||
|
||||
{{ spark_bar_field(item.count, most_used_template_count, id=item.template_id) }}
|
||||
{% endcall %}
|
||||
<a
|
||||
href="{{ url_for('.template_usage', service_id=current_service.id) }}"
|
||||
class="usa-link show-more-no-border"
|
||||
><span>See templates used by month</span></a>
|
||||
{% if template_statistics|length > 1 %}
|
||||
<h2 class="margin-top-4 margin-bottom-1">Most Used Templates</h2>
|
||||
<div class="template-statistics-table table-overflow-x-auto">
|
||||
<table class="usa-table width-full">
|
||||
<caption class="font-body-lg table-heading usa-sr-only">
|
||||
Messages sent by template
|
||||
</caption>
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th class="table-field-heading-first" width="">
|
||||
<span>Template name</span>
|
||||
</th>
|
||||
<th class="table-field-heading" width="">
|
||||
<span>Folder</span>
|
||||
</th>
|
||||
<th class="table-field-heading" width="">
|
||||
<span>Last used</span>
|
||||
</th>
|
||||
<th class="table-field-heading" width="">
|
||||
<span>Created by</span>
|
||||
</th>
|
||||
<th class="table-field-heading" width="">
|
||||
<span># Times used</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in template_statistics[:8] %}
|
||||
<tr class="table-row">
|
||||
<td>
|
||||
<a class="usa-link template-statistics-table-template-name" href="{{ url_for('.view_template', service_id=current_service.id, template_id=item.template_id) }}">{{ item.template_name }}</a>
|
||||
</td>
|
||||
<td><p>{{ item.template_folder }}</p></td>
|
||||
<td><p>{{ item.last_used|format_datetime_table}}</p></td>
|
||||
<td><p>{{ item.created_by }}</p></td>
|
||||
<td><p>{{ item.count }}</p></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="/services/78409625-0c0a-485e-b82c-b19c8f4b1bdb/template-usage" class="usa-link show-more-no-border"><span>See templates by month</span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -16,17 +16,22 @@
|
||||
<p>Below we explain how to:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
{% if not feature_best_practices_enabled %}
|
||||
<li><a class="usa-link" href="#format-content">Format your content</a></li>
|
||||
<li><a class="usa-link" href="#add-links">Add links</a></li>
|
||||
{% endif %}
|
||||
<li><a class="usa-link" href="#personalize-content">Personalize your content</a></li>
|
||||
<li><a class="usa-link" href="#conditional-content">Add conditional content</a></li>
|
||||
<li><a class="usa-link" href="#indentify-program">Identify your program</a></li>
|
||||
<li><a class="usa-link" href="#prepare-data">Prepare your data</a></li>
|
||||
{% if not feature_best_practices_enabled %}
|
||||
<li><a class="usa-link" href="#prevent-fraud">Prevent fraud</a></li>
|
||||
{% endif%}
|
||||
</ul>
|
||||
|
||||
{# Format content #}
|
||||
<h2 class="padding-top-1" id="format-content">Format your content</h2>
|
||||
{% if not feature_best_practices_enabled %}
|
||||
<p>Effective texts will help your message recipients take the steps needed to secure and keep the benefits and services
|
||||
they depend on. To craft an effective text:</p>
|
||||
|
||||
@@ -37,7 +42,7 @@
|
||||
<li>Say only one important thing per message.</li>
|
||||
<li>Frame the message to encourage action and explain the consequences of not completing the desired action.</li>
|
||||
</ul>
|
||||
|
||||
{% endif %}
|
||||
<h3>To create and format your message</h3>
|
||||
<ol class="list">
|
||||
<li>All messages start from a template</li>
|
||||
@@ -45,6 +50,7 @@
|
||||
<li>Add a new template or choose an existing template and select Edit.</li>
|
||||
</ol>
|
||||
|
||||
{% if not feature_best_practices_enabled %}
|
||||
{# Add links #}
|
||||
<h2 class="padding-top-1" id="add-links">Add links</h2>
|
||||
<p>When composing a text message, links to websites or online applications can help your recipient respond quickly.</p>
|
||||
@@ -64,6 +70,7 @@
|
||||
<li>Your link might stop working if there’s a service outage.</li>
|
||||
<li>You can no longer control where the redirect goes.</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{# Personalize content #}
|
||||
<h2 class="padding-top-1" id="personalize-content">Personalize your content</h2>
|
||||
@@ -214,6 +221,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if not feature_best_practices_enabled %}
|
||||
{# Preventing fraud #}
|
||||
<h2 class="padding-top-1" id="prevent-fraud">Preventing fraud</h2>
|
||||
<p>Texting fraud is ever prevalent and while we can’t eliminate the risk, we can reduce the chances that recipients will
|
||||
@@ -240,5 +248,5 @@
|
||||
in a text. If you have questions about how to protect your privacy, see statename.gov/privacy.</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% block serviceNavigation %}{% endblock %}
|
||||
|
||||
{% block sideNavigation %}
|
||||
{% include "/new/components/settings_nav.html" %}
|
||||
{% include "components/settings_nav.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{% block backLink %}{% endblock %}
|
||||
<main id="main-content" role="main">
|
||||
{% block content %}
|
||||
{% include 'new/components/flash_messages.html' %}
|
||||
{% include 'components/flash_messages.html' %}
|
||||
{% block platform_admin_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
{% if not service['active'] %}
|
||||
<span class="heading-medium hint"> Archived</span>
|
||||
{% endif %}
|
||||
{% if service['name'] == 'Test service' %}
|
||||
<a class="usa-link" href="{{ url_for('main.load_test') }}">Load Test</a>
|
||||
{% endif %}
|
||||
|
||||
{% endcall %}
|
||||
|
||||
{% endcall %}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<ul class="list list-bullet">
|
||||
<li>Achieve compliance to begin piloting, such as ATO and privacy standards.
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
use xlink:href="/assets/img/sprite.svg#check"></use>
|
||||
<use xlink:href="/assets/img/sprite.svg#check"></use>
|
||||
</svg>
|
||||
</li>
|
||||
<li>Demonstrate that a government-run notifications tool provides a unique value.</li>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% from "components/file-upload.html" import file_upload %}
|
||||
{% from "components/table.html" import list_table, text_field, index_field, index_field_heading %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
||||
|
||||
{% block service_page_title %}
|
||||
Upload a list of {{ 999|recipient_count_label(template.template_type) }}
|
||||
|
||||