mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-24 00:07:02 -04:00
Compare commits
522 Commits
07-24-2024
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02457af52d | ||
|
|
0d04524525 | ||
|
|
40a0418731 | ||
|
|
6ff8be5d3e | ||
|
|
2c7d090b82 | ||
|
|
4f193e362c | ||
|
|
302a79b41f | ||
|
|
bdc5614fff | ||
|
|
a5e72ea4eb | ||
|
|
63747515db | ||
|
|
55f588f13d | ||
|
|
81a629935c | ||
|
|
49ec3dc559 | ||
|
|
647b37ae61 | ||
|
|
7b2d495c1b | ||
|
|
47f0952125 | ||
|
|
e0fb031cf5 | ||
|
|
72eff3235e | ||
|
|
67ad9148d6 | ||
|
|
4cdac9eba3 | ||
|
|
76863cd4dc | ||
|
|
3d00b0c94f | ||
|
|
4594dc317f | ||
|
|
5e93f2d572 | ||
|
|
a2dbb6c9e6 | ||
|
|
d097a0e2a6 | ||
|
|
d3855f47ac | ||
|
|
6f91d60030 | ||
|
|
99ce42fc4a | ||
|
|
899347d349 | ||
|
|
14b37524ae | ||
|
|
5caa26e93c | ||
|
|
5ee559da10 | ||
|
|
73852a54f2 | ||
|
|
ec806ce385 | ||
|
|
8c3cdabb92 | ||
|
|
a5889d3448 | ||
|
|
09a775fdad | ||
|
|
62cfa2c39c | ||
|
|
8b6f719510 | ||
|
|
b953d316c4 | ||
|
|
5c3b5790ae | ||
|
|
d0d3878881 | ||
|
|
b6e0249adb | ||
|
|
4a8d768b6f | ||
|
|
876db282d5 | ||
|
|
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 | ||
|
|
c5149f52d4 | ||
|
|
529f5e76ba | ||
|
|
f30363b473 | ||
|
|
0b7183a29d | ||
|
|
3a38c1f94b | ||
|
|
7962bfd85f | ||
|
|
d495840a67 | ||
|
|
154e58a83d | ||
|
|
45e4910372 | ||
|
|
68fbdb2bb8 | ||
|
|
6175bfb2fb | ||
|
|
c2cb8e8f85 | ||
|
|
1a76ff5dbe | ||
|
|
cbffcff1cd | ||
|
|
4960a49d11 | ||
|
|
3a43dce715 | ||
|
|
b9d1ae5ad8 | ||
|
|
a31e199147 | ||
|
|
46124a45cc | ||
|
|
56d9970ebe | ||
|
|
3ce9097a02 | ||
|
|
d57531a2b5 | ||
|
|
44126a2f0c | ||
|
|
49e390dd5e | ||
|
|
f17ee0147f | ||
|
|
3397b0c380 | ||
|
|
9a59a09bf8 | ||
|
|
0fabcabeb3 | ||
|
|
6e77caee1a | ||
|
|
b5d4e12466 | ||
|
|
3a647ffa91 | ||
|
|
832f18a1a9 | ||
|
|
556e381677 | ||
|
|
0391c94677 | ||
|
|
5d211f7c59 | ||
|
|
e3c78c5871 | ||
|
|
c5b71dfe8d | ||
|
|
847057b2a8 | ||
|
|
932963beb5 | ||
|
|
d10a46c5ee | ||
|
|
cf5ea736e2 | ||
|
|
aa1a738463 | ||
|
|
bfb08173c2 | ||
|
|
46c11285c6 | ||
|
|
57ee345451 | ||
|
|
6d09d44a84 | ||
|
|
a0e177b99b | ||
|
|
96d6415578 | ||
|
|
bda957f76a | ||
|
|
6cb03d962e | ||
|
|
2f62a57c7c | ||
|
|
de27071112 | ||
|
|
2cbab8727f | ||
|
|
a6e7e15643 | ||
|
|
9e6f87ace1 | ||
|
|
7d22f23217 | ||
|
|
75a69dc8a6 | ||
|
|
191b591ac0 | ||
|
|
e5b4dd5a31 | ||
|
|
d9d802cfb7 | ||
|
|
cb8e620343 | ||
|
|
393ba67878 | ||
|
|
39981c1f70 | ||
|
|
0a39077fc8 | ||
|
|
220106ff6b | ||
|
|
832932f3b8 | ||
|
|
1acb37c012 | ||
|
|
45bc6a9173 | ||
|
|
3e472e0a97 | ||
|
|
9b768846cb | ||
|
|
a395ad0281 | ||
|
|
28bc4befbe | ||
|
|
193e1f7952 | ||
|
|
38a6c35afd | ||
|
|
ee1dcdeb03 | ||
|
|
25f75a98ec | ||
|
|
cc310d8a23 | ||
|
|
e730c46525 | ||
|
|
7d6b209c8d | ||
|
|
064cae7c31 | ||
|
|
a3d9c24d34 | ||
|
|
c778ad092b | ||
|
|
f3cab5a6cc | ||
|
|
b6dd9967ed | ||
|
|
b654bca6bf | ||
|
|
1a75e56187 | ||
|
|
21da7ef7db | ||
|
|
69e9c6d1ca | ||
|
|
3bafa48577 | ||
|
|
f8e85e5fa6 | ||
|
|
d1701ab946 | ||
|
|
b426a17cf8 | ||
|
|
86748c3317 | ||
|
|
1a75fc5142 | ||
|
|
2bb398a962 | ||
|
|
92026a83aa | ||
|
|
a6ac7086da | ||
|
|
c6f1ae37a4 | ||
|
|
f65655e56c | ||
|
|
5797e989fd | ||
|
|
4bfbd9f71f | ||
|
|
65d339fb8a | ||
|
|
0f75fbf11e | ||
|
|
b1668ebe57 | ||
|
|
e7e3bac36c | ||
|
|
b77a142f6b | ||
|
|
2497679046 | ||
|
|
078fbb7f4e | ||
|
|
ad2151b637 | ||
|
|
aed9778a65 | ||
|
|
1a528626de | ||
|
|
323ecf63f4 | ||
|
|
b01ca6bb15 | ||
|
|
7b03d02447 | ||
|
|
89eb24c795 | ||
|
|
6cecd5903a | ||
|
|
a48f4fa476 | ||
|
|
cf505faef9 | ||
|
|
7c89863eab | ||
|
|
bdbdaaae8a | ||
|
|
6e3ad6b354 | ||
|
|
d6079a463d | ||
|
|
cd1cedad69 | ||
|
|
bc853367d9 | ||
|
|
9f79ae314f | ||
|
|
7ac6400827 | ||
|
|
af59b13836 | ||
|
|
79786df09c | ||
|
|
863979b726 | ||
|
|
0fe73f6973 | ||
|
|
65da623458 | ||
|
|
6b8be20ed2 | ||
|
|
27ee039b2d | ||
|
|
03466d0184 | ||
|
|
131d99a4df | ||
|
|
136e82102b | ||
|
|
b84d00bfb1 | ||
|
|
22bc355562 | ||
|
|
9471174f20 | ||
|
|
da33213730 | ||
|
|
da34acaf76 | ||
|
|
97ae46d57b | ||
|
|
989aaf747f | ||
|
|
fd046e02e7 | ||
|
|
42db905026 | ||
|
|
0ea4d8897e | ||
|
|
7715e6b275 | ||
|
|
cfc65d20c3 | ||
|
|
634f3f9643 | ||
|
|
71cca74779 | ||
|
|
a1180747d5 | ||
|
|
d12d2418a5 | ||
|
|
5680f5c98f | ||
|
|
6225181087 | ||
|
|
cbb4254204 | ||
|
|
6a5b033700 | ||
|
|
4df7eb57d7 | ||
|
|
86a2017f91 | ||
|
|
a8fad883f1 | ||
|
|
1912ffbbcf | ||
|
|
ae02972c6d | ||
|
|
55e1664d04 | ||
|
|
d42cadd3b1 | ||
|
|
9638067e63 | ||
|
|
99493acb86 | ||
|
|
d3de0bb01d | ||
|
|
044175bac6 | ||
|
|
78564acea7 | ||
|
|
cbfeca5a1f | ||
|
|
5eb9bcc843 | ||
|
|
447046697e | ||
|
|
84657a691f | ||
|
|
4b71b2550a | ||
|
|
4982452bec | ||
|
|
3833533b5c | ||
|
|
e637a8177c | ||
|
|
e884f31fa0 | ||
|
|
34d727748b | ||
|
|
765920ef0a | ||
|
|
0c1f824ca3 | ||
|
|
883b39cd93 | ||
|
|
2933ded509 | ||
|
|
8fb019b93d | ||
|
|
8461c6156b | ||
|
|
42184a17cd | ||
|
|
d68df54bb9 | ||
|
|
dfcd71a6d3 | ||
|
|
b37b95cdcf | ||
|
|
a8ce0be98b | ||
|
|
bd03d273fe | ||
|
|
e900a7c81f | ||
|
|
37c5f84ee1 | ||
|
|
6e398e7a39 | ||
|
|
88435155a5 | ||
|
|
68d58116c4 | ||
|
|
2daaa6dd38 | ||
|
|
4e558ca0e7 | ||
|
|
55f1939fd5 | ||
|
|
a4e1446791 | ||
|
|
a7cbccc209 | ||
|
|
3c30a14c53 | ||
|
|
508dfc5d84 | ||
|
|
1f9cccede6 | ||
|
|
c7b90e3010 | ||
|
|
1bdb291397 | ||
|
|
b8c1373dde | ||
|
|
ad2e5de01b | ||
|
|
5b1516bdd1 | ||
|
|
34ae71716b | ||
|
|
5d287d236e | ||
|
|
3dcc438542 | ||
|
|
a0e64304d6 | ||
|
|
4af04ff183 | ||
|
|
febd037bed | ||
|
|
5cf05bd4b6 | ||
|
|
5f5f6a948b | ||
|
|
c80645b11c | ||
|
|
20ab840f27 | ||
|
|
2029c64ea6 | ||
|
|
b3de594741 | ||
|
|
afd82f3742 | ||
|
|
2e81f6f45a | ||
|
|
47bb1e4c8c | ||
|
|
f0b4fbe732 | ||
|
|
4113a2819d | ||
|
|
b718fbf791 | ||
|
|
ba2b5fea37 | ||
|
|
d301502957 | ||
|
|
8a3bdd181d | ||
|
|
7f288598ba | ||
|
|
e8f274baa2 | ||
|
|
2e20b20912 | ||
|
|
a81dc83133 | ||
|
|
0b2868a8d3 | ||
|
|
74f4d730a8 | ||
|
|
e5204ef7c5 | ||
|
|
731a79fff8 | ||
|
|
0a17aaad0d | ||
|
|
149c23b721 | ||
|
|
cc24604aa4 | ||
|
|
30eac798c2 | ||
|
|
55fe08beca | ||
|
|
24252ecd39 | ||
|
|
20cb8021b8 | ||
|
|
e06c855f8e | ||
|
|
90ed600615 | ||
|
|
6c44a297b8 | ||
|
|
fc15ab6a9f | ||
|
|
edbbb7289f | ||
|
|
c5a467c417 | ||
|
|
cef5f921c2 | ||
|
|
f979d41565 | ||
|
|
ea8cfbdadb | ||
|
|
0f738c8cb7 | ||
|
|
d1ed7e1d0c | ||
|
|
468f6c9fc6 | ||
|
|
9af16b443a | ||
|
|
abcbb0eb7a | ||
|
|
9d22b99e9b | ||
|
|
9b84f59bf8 | ||
|
|
b9beb50313 | ||
|
|
726e8db5b2 | ||
|
|
b759f8a87f | ||
|
|
ae56ea1482 | ||
|
|
1f84db7861 | ||
|
|
568400941b | ||
|
|
c02c9152d8 | ||
|
|
30b31d61b6 | ||
|
|
f9169fcbcb | ||
|
|
ae78e5868a | ||
|
|
bbbaf592aa | ||
|
|
17b21b2382 | ||
|
|
8331456318 | ||
|
|
f6e7402be9 | ||
|
|
4b622fcfdc | ||
|
|
40d62e5ff9 | ||
|
|
2a13aba768 | ||
|
|
5f4804f65a | ||
|
|
473eefb5b0 | ||
|
|
04f0a173ac | ||
|
|
de37442aa0 | ||
|
|
e49ee06ffa | ||
|
|
c6a116c9bb | ||
|
|
ccfc1166a7 | ||
|
|
dcc7679380 | ||
|
|
f517180581 | ||
|
|
b9d41dad86 | ||
|
|
04a7b57990 | ||
|
|
afdc0ed58d | ||
|
|
170527cfdf | ||
|
|
7b570bfbed | ||
|
|
bc72288d4d | ||
|
|
e50b4bb5f7 | ||
|
|
32f1bb4f87 | ||
|
|
594ad74006 | ||
|
|
3b9664f220 | ||
|
|
9897385837 | ||
|
|
4ec3bc5892 | ||
|
|
358971164e | ||
|
|
282611ec9c | ||
|
|
4a6539197f | ||
|
|
e92fd011ca | ||
|
|
9e0fe69d2b | ||
|
|
0ce3d5033c | ||
|
|
ff110de97f | ||
|
|
d54c339994 | ||
|
|
3dc73ab92e | ||
|
|
42570416da | ||
|
|
e934e49137 | ||
|
|
cec1e4cff0 | ||
|
|
31762239dd | ||
|
|
017198754f |
32
.ds.baseline
32
.ds.baseline
@@ -133,15 +133,7 @@
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 67,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Basic Auth Credentials",
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 101,
|
||||
"line_number": 66,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -169,7 +161,7 @@
|
||||
"filename": "app/config.py",
|
||||
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
|
||||
"is_verified": false,
|
||||
"line_number": 117,
|
||||
"line_number": 118,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -407,23 +399,13 @@
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/main/views/sign_in.py": [
|
||||
{
|
||||
"type": "Private Key",
|
||||
"filename": "app/main/views/sign_in.py",
|
||||
"hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9",
|
||||
"is_verified": false,
|
||||
"line_number": 27,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/templates/new/components/head.html": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/templates/new/components/head.html",
|
||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||
"is_verified": false,
|
||||
"line_number": 34,
|
||||
"line_number": 33,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -545,7 +527,7 @@
|
||||
"filename": "tests/app/main/views/test_accept_invite.py",
|
||||
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
|
||||
"is_verified": false,
|
||||
"line_number": 626,
|
||||
"line_number": 643,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -611,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
|
||||
},
|
||||
{
|
||||
@@ -619,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
|
||||
}
|
||||
],
|
||||
@@ -702,5 +684,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2024-07-11T16:37:23Z"
|
||||
"generated_at": "2024-09-03T17:36:57Z"
|
||||
}
|
||||
|
||||
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
@@ -3,14 +3,22 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 3
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
assignees:
|
||||
- "A-Shumway42"
|
||||
reviewers:
|
||||
- "A-Shumway42"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
|
||||
interval: "daily"
|
||||
versioning-strategy: increase
|
||||
assignees:
|
||||
- "alexjanousekGSA"
|
||||
reviewers:
|
||||
- "alexjanousekGSA"
|
||||
|
||||
58
.github/workflows/checks.yml
vendored
58
.github/workflows/checks.yml
vendored
@@ -44,8 +44,6 @@ jobs:
|
||||
run: poetry run isort --check-only ./app ./tests
|
||||
- name: Check dead code
|
||||
run: make dead-code
|
||||
- name: Run js lint
|
||||
run: npm run lint
|
||||
- name: Run js tests
|
||||
run: npm test
|
||||
- name: Run py tests with coverage
|
||||
@@ -54,6 +52,7 @@ jobs:
|
||||
run: poetry run coverage report --fail-under=90
|
||||
|
||||
end-to-end-tests:
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
@@ -84,41 +83,25 @@ jobs:
|
||||
ports:
|
||||
# Maps tcp port 6379 on service container to the host
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- name: 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 &
|
||||
make e2e-test
|
||||
env:
|
||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov
|
||||
API_HOST_NAME: http://localhost:6011
|
||||
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
@@ -127,20 +110,7 @@ jobs:
|
||||
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:
|
||||
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/
|
||||
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/deploy-demo.yml
vendored
2
.github/workflows/deploy-demo.yml
vendored
@@ -63,6 +63,7 @@ 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 }}
|
||||
@@ -85,6 +86,7 @@ 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"
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
|
||||
2
.github/workflows/deploy-prod.yml
vendored
2
.github/workflows/deploy-prod.yml
vendored
@@ -63,6 +63,7 @@ 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 }}
|
||||
@@ -85,6 +86,7 @@ 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"
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
|
||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -69,6 +69,7 @@ jobs:
|
||||
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_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -91,6 +92,7 @@ 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"
|
||||
|
||||
|
||||
- name: Check for changes to egress config
|
||||
|
||||
2
Makefile
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
|
||||
|
||||
@@ -18,7 +18,6 @@ from flask import (
|
||||
)
|
||||
from flask.globals import request_ctx
|
||||
from flask_login import LoginManager, current_user
|
||||
from flask_socketio import SocketIO
|
||||
from flask_talisman import Talisman
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_wtf.csrf import CSRFError
|
||||
@@ -119,8 +118,6 @@ from notifications_utils.recipients import format_phone_number_human_readable
|
||||
login_manager = LoginManager()
|
||||
csrf = CSRFProtect()
|
||||
talisman = Talisman()
|
||||
socketio = SocketIO()
|
||||
|
||||
|
||||
# The current service attached to the request stack.
|
||||
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
||||
@@ -177,7 +174,6 @@ def create_app(application):
|
||||
|
||||
init_govuk_frontend(application)
|
||||
init_jinja(application)
|
||||
socketio.init_app(application)
|
||||
|
||||
for client in (
|
||||
csrf,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
22
app/assets/images/gsa-logo.svg
Normal file
22
app/assets/images/gsa-logo.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="719px" height="720px" viewBox="0 0 129.996 130.238" enable-background="new 0 0 129.996 130.238"
|
||||
xml:space="preserve">
|
||||
<rect fill="#00538E" width="129.996" height="130.238"/>
|
||||
<path fill="#FFFFFF" d="M78.611,96.076c0,9.138-7.175,12.953-15.451,12.688c-2.938-0.095-5.799-0.838-8.916-1.855l0.707-6.578
|
||||
c2.638,1.086,6.057,2.263,8.92,2.219c3.74-0.058,7.477-1.794,7.477-6.124c0-3.335-2.67-4.976-5.252-6.283
|
||||
c-5.709-2.896-12.275-4.907-12.275-12.998c0-7.65,6.084-11.64,13.625-11.64c2.741,0,6.014,0.477,9.299,1.641l-0.985,6.478
|
||||
c-3.577-1.403-5.14-1.901-8.078-1.901c-3.253,0-6.597,1.143-6.597,4.896c0,2.933,3.01,4.456,5.248,5.573
|
||||
C72.398,85.218,78.611,87.703,78.611,96.076"/>
|
||||
<path fill="#FFFFFF" d="M44.561,106.8c-2.632,0.901-6.662,1.964-13.092,1.964c-13.195,0-22.027-8.529-22.027-21.805
|
||||
c0-12.882,9.375-21.455,22.086-21.455c6.036,0,8.727,0.896,12.541,2.212l-0.574,7.216c-3.51-2.294-6.498-3.211-11.674-3.211
|
||||
c-9.09,0-15.158,6.578-15.115,15.412c0.043,9.506,6.648,15.416,14.822,15.416c2.437,0,4.508-0.178,5.996-0.569V91.15h-7.861v-6.103
|
||||
h14.898V106.8"/>
|
||||
<polyline fill="#FFFFFF" points="102.041,64.587 112.063,86.918 105.08,86.918 102.041,79.497 98.996,86.918 92.014,86.918
|
||||
102.041,64.587 "/>
|
||||
<polyline fill="#FFFFFF" points="112.178,87.179 121.507,108.029 114.176,108.029 107.203,91.839 112.178,87.179 "/>
|
||||
<polyline fill="#FFFFFF" points="91.9,87.183 96.873,91.841 93.199,100.24 102.027,95.611 107.109,99.525 89.727,108.031
|
||||
82.57,108.031 91.9,87.183 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,712 +0,0 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 1129 1483" enable-background="new 0 0 1129 1483" xml:space="preserve">
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M539.000000,1484.000000
|
||||
C359.711578,1484.000000 180.423172,1484.000000 1.000000,1484.000000
|
||||
C1.000000,1105.639771 1.000000,727.279602 1.410214,348.316467
|
||||
C4.569818,348.140381 7.319019,348.939209 10.068629,348.940613
|
||||
C131.539047,349.002533 253.009537,349.019958 374.479858,348.902496
|
||||
C379.495209,348.897644 382.964447,350.180725 385.438477,354.685120
|
||||
C387.083405,357.679993 389.360779,360.359558 391.555206,363.014832
|
||||
C400.272888,373.563141 409.100433,384.020660 417.827881,394.560944
|
||||
C426.754944,405.342346 435.596100,416.194855 444.505066,426.991333
|
||||
C453.315704,437.668549 462.350189,448.169037 470.874084,459.069550
|
||||
C473.211273,462.058380 475.868622,462.668365 478.552521,461.668732
|
||||
C479.870514,461.177765 480.891022,458.075714 480.899139,456.150421
|
||||
C481.039185,422.992401 480.992554,389.833649 481.006348,356.675049
|
||||
C481.009247,349.746185 481.792328,348.996246 488.738434,348.995605
|
||||
C524.063171,348.992462 559.387878,348.997498 594.712646,348.991364
|
||||
C602.408508,348.990021 602.999207,348.399902 602.999329,340.867279
|
||||
C603.000366,227.578186 603.000000,114.289093 603.000000,1.000000
|
||||
C778.587646,1.000000 954.175293,1.000000 1130.000000,1.000000
|
||||
C1130.000000,136.026779 1130.000000,271.053680 1129.661621,406.458130
|
||||
C1128.681885,406.105896 1127.790039,405.480469 1127.435913,404.630920
|
||||
C1120.489746,387.961334 1107.422241,381.006897 1090.001465,381.005951
|
||||
C910.382629,380.996246 730.763794,380.970703 551.145081,381.139465
|
||||
C545.655090,381.144623 539.914307,382.532288 534.729980,384.458374
|
||||
C523.458130,388.646179 512.165527,404.262543 512.742798,416.804962
|
||||
C513.691650,437.422455 513.000000,458.115448 513.000000,478.776611
|
||||
C513.000000,802.357117 512.996948,1125.937622 513.047546,1449.518188
|
||||
C513.047974,1452.454102 513.316040,1455.538330 514.227844,1458.299561
|
||||
C517.778442,1469.052002 525.487427,1476.064087 535.397400,1481.004028
|
||||
C536.757935,1481.682251 537.806824,1482.985840 539.000000,1484.000000
|
||||
z"/>
|
||||
<path fill="#005EA2" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M602.531372,1.000000
|
||||
C603.000000,114.289093 603.000366,227.578186 602.999329,340.867279
|
||||
C602.999207,348.399902 602.408508,348.990021 594.712646,348.991364
|
||||
C559.387878,348.997498 524.063171,348.992462 488.738434,348.995605
|
||||
C481.792328,348.996246 481.009247,349.746185 481.006348,356.675049
|
||||
C480.992554,389.833649 481.039185,422.992401 480.899139,456.150421
|
||||
C480.891022,458.075714 479.870514,461.177765 478.552521,461.668732
|
||||
C475.868622,462.668365 473.211273,462.058380 470.874084,459.069550
|
||||
C462.350189,448.169037 453.315704,437.668549 444.505066,426.991333
|
||||
C435.596100,416.194855 426.754944,405.342346 417.827881,394.560944
|
||||
C409.100433,384.020660 400.272888,373.563141 391.555206,363.014832
|
||||
C389.360779,360.359558 387.083405,357.679993 385.438477,354.685120
|
||||
C382.964447,350.180725 379.495209,348.897644 374.479858,348.902496
|
||||
C253.009537,349.019958 131.539047,349.002533 10.068629,348.940613
|
||||
C7.319019,348.939209 4.569818,348.140381 1.410214,347.856750
|
||||
C1.000000,232.386658 1.000000,116.773323 1.000000,1.000000
|
||||
C201.354202,1.000000 401.708435,1.000000 602.531372,1.000000
|
||||
z"/>
|
||||
<path fill="#1B1B1B" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M539.468689,1484.000000
|
||||
C537.806824,1482.985840 536.757935,1481.682251 535.397400,1481.004028
|
||||
C525.487427,1476.064087 517.778442,1469.052002 514.227844,1458.299561
|
||||
C513.316040,1455.538330 513.047974,1452.454102 513.047546,1449.518188
|
||||
C512.996948,1125.937622 513.000000,802.357117 513.000000,478.776611
|
||||
C513.000000,458.115448 513.691650,437.422455 512.742798,416.804962
|
||||
C512.165527,404.262543 523.458130,388.646179 534.729980,384.458374
|
||||
C539.914307,382.532288 545.655090,381.144623 551.145081,381.139465
|
||||
C730.763794,380.970703 910.382629,380.996246 1090.001465,381.005951
|
||||
C1107.422241,381.006897 1120.489746,387.961334 1127.435913,404.630920
|
||||
C1127.790039,405.480469 1128.681885,406.105896 1129.661621,406.917847
|
||||
C1130.000000,756.687561 1130.000000,1106.375122 1129.663086,1456.612671
|
||||
C1128.761597,1457.741333 1127.970459,1458.220581 1127.666382,1458.913574
|
||||
C1123.083496,1469.360596 1116.098755,1477.361450 1105.191040,1481.552490
|
||||
C1104.281616,1481.901978 1103.722046,1483.162598 1103.000000,1484.000000
|
||||
C915.312439,1484.000000 727.624878,1484.000000 539.468689,1484.000000
|
||||
M715.500000,1342.999756
|
||||
C840.560608,1342.999756 965.621216,1342.999756 1090.580322,1342.999756
|
||||
C1090.580322,1053.180908 1090.580322,764.298523 1090.580322,475.379303
|
||||
C910.605164,475.379303 731.049866,475.379303 551.330566,475.379303
|
||||
C551.330566,764.641113 551.330566,1053.523560 551.330566,1342.999756
|
||||
C605.939392,1342.999756 660.219727,1342.999756 715.500000,1342.999756
|
||||
M848.995056,1439.501953
|
||||
C861.071411,1426.778076 864.464417,1411.987305 857.870850,1395.782471
|
||||
C851.682190,1380.572754 839.614075,1372.216919 823.238037,1371.120239
|
||||
C804.724487,1369.880737 788.393555,1381.497681 782.851562,1399.040405
|
||||
C777.381104,1416.357056 784.187378,1434.929688 799.618713,1444.794189
|
||||
C814.984253,1454.616455 834.419617,1452.711060 848.995056,1439.501953
|
||||
M827.500000,439.001709
|
||||
C837.987183,439.001678 848.474426,439.019501 858.961548,438.995575
|
||||
C866.404968,438.978607 871.095825,434.951721 870.987549,428.742432
|
||||
C870.884216,422.809875 866.245544,419.012268 858.947205,419.006348
|
||||
C833.644836,418.985809 808.342407,418.987823 783.040039,419.004883
|
||||
C775.608643,419.009918 770.903564,423.058716 771.012451,429.256409
|
||||
C771.116821,435.194092 775.748901,438.980286 783.053101,438.993744
|
||||
C797.535339,439.020386 812.017700,439.001678 827.500000,439.001709
|
||||
M725.402283,419.003754
|
||||
C716.655090,419.947510 712.145569,427.803436 716.640198,434.366302
|
||||
C719.365906,438.346191 723.255737,439.998871 727.861145,438.552002
|
||||
C732.381165,437.131927 734.912048,433.752899 734.979553,428.883942
|
||||
C735.046692,424.037842 732.110413,420.836304 725.402283,419.003754
|
||||
z"/>
|
||||
<path fill="#000000" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M1103.468628,1484.000000
|
||||
C1103.722046,1483.162598 1104.281616,1481.901978 1105.191040,1481.552490
|
||||
C1116.098755,1477.361450 1123.083496,1469.360596 1127.666382,1458.913574
|
||||
C1127.970459,1458.220581 1128.761597,1457.741333 1129.663086,1457.081299
|
||||
C1130.000000,1465.926392 1130.000000,1474.852905 1130.000000,1484.000000
|
||||
C1121.313721,1484.000000 1112.625488,1484.000000 1103.468628,1484.000000
|
||||
z"/>
|
||||
<path fill="#005EA2" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M715.000000,1342.999756
|
||||
C660.219727,1342.999756 605.939392,1342.999756 551.330566,1342.999756
|
||||
C551.330566,1053.523560 551.330566,764.641113 551.330566,475.379303
|
||||
C731.049866,475.379303 910.605164,475.379303 1090.580322,475.379303
|
||||
C1090.580322,764.298523 1090.580322,1053.180908 1090.580322,1342.999756
|
||||
C965.621216,1342.999756 840.560608,1342.999756 715.000000,1342.999756
|
||||
M757.917419,532.016174
|
||||
C757.710449,531.842957 757.503540,531.669800 757.225159,530.601135
|
||||
C756.969604,527.453918 758.218384,526.051575 761.558044,526.301270
|
||||
C763.955505,526.480469 766.398010,526.057617 768.820374,525.903442
|
||||
C768.792847,525.411682 768.765259,524.919922 768.737732,524.428162
|
||||
C764.196594,524.428162 759.655518,524.428162 755.034241,524.428162
|
||||
C754.709106,527.381104 754.389282,530.187317 754.093750,532.996155
|
||||
C753.744568,536.314331 754.919922,537.421631 758.021118,535.657959
|
||||
C761.182068,533.860352 764.437500,533.753479 766.141663,537.173096
|
||||
C766.929382,538.753906 766.444580,542.116272 765.214417,543.323975
|
||||
C762.385193,546.101440 759.204468,545.085571 756.479553,542.327881
|
||||
C755.735840,541.575317 754.459167,541.349365 752.383667,540.408936
|
||||
C755.843872,546.609497 760.206726,548.611938 765.034119,546.895874
|
||||
C769.052124,545.467529 770.526672,542.303223 770.059326,538.198303
|
||||
C769.479248,533.103760 765.369141,531.126282 757.917419,532.016174
|
||||
M716.917419,532.016174
|
||||
C716.710449,531.842957 716.503540,531.669800 716.225159,530.601135
|
||||
C715.969604,527.453918 717.218384,526.051575 720.558044,526.301270
|
||||
C722.955505,526.480469 725.398010,526.057617 727.820374,525.903442
|
||||
C727.792847,525.411682 727.765259,524.919922 727.737732,524.428162
|
||||
C723.196594,524.428162 718.655518,524.428162 714.034180,524.428162
|
||||
C713.709106,527.381104 713.389282,530.187317 713.093689,532.996155
|
||||
C712.744568,536.314331 713.919922,537.421631 717.021118,535.657959
|
||||
C720.182068,533.860352 723.437500,533.753479 725.141663,537.173096
|
||||
C725.929443,538.753906 725.444580,542.116272 724.214417,543.323975
|
||||
C721.385193,546.101440 718.204468,545.085571 715.479553,542.327942
|
||||
C714.735840,541.575317 713.459167,541.349426 711.383728,540.408997
|
||||
C714.843872,546.609497 719.206787,548.612000 724.034119,546.895996
|
||||
C728.052124,545.467590 729.526733,542.303223 729.059387,538.198303
|
||||
C728.479309,533.103760 724.369141,531.126282 716.917419,532.016174
|
||||
M1058.999390,737.500000
|
||||
C1058.999390,697.505127 1058.999512,657.510193 1058.999268,617.515320
|
||||
C1058.999268,605.743347 1056.299927,603.000366 1044.711548,603.000366
|
||||
C895.564270,603.000061 746.416931,603.000061 597.269653,603.000305
|
||||
C585.816956,603.000305 583.000000,605.768921 582.999939,617.030762
|
||||
C582.999451,736.348633 582.999512,855.666443 582.999878,974.984314
|
||||
C582.999939,986.054382 585.894104,988.999268 596.785095,988.999329
|
||||
C746.265686,989.000183 895.746216,989.000061 1045.226807,988.999451
|
||||
C1056.137695,988.999451 1058.998901,986.083923 1058.998901,974.969299
|
||||
C1058.999512,896.146179 1058.999390,817.323120 1058.999390,737.500000
|
||||
M838.417786,528.949585
|
||||
C839.083923,531.005493 839.750061,533.061462 840.461304,535.256653
|
||||
C836.744202,539.386841 836.324524,542.075928 839.096497,544.925293
|
||||
C842.052917,547.964355 849.674805,548.159302 852.937805,545.279297
|
||||
C856.009766,542.567871 855.691833,539.842896 851.688904,534.575378
|
||||
C855.016052,530.149902 854.543030,526.284058 850.432739,524.310608
|
||||
C846.111816,522.236023 841.257324,523.850464 838.417786,528.949585
|
||||
M745.891296,542.169128
|
||||
C741.158203,548.675415 737.081299,541.854736 732.290100,540.637451
|
||||
C734.869629,546.392883 739.339478,548.482056 744.370422,546.980408
|
||||
C748.078552,545.873596 750.549316,541.850952 749.525513,537.959473
|
||||
C749.089172,536.301086 748.107056,533.978394 746.814453,533.514832
|
||||
C743.774109,532.424500 740.397705,532.271240 736.826416,531.684448
|
||||
C736.926086,530.342651 737.066406,528.453491 737.205750,526.577209
|
||||
C741.252563,526.391479 744.746399,526.231140 748.240173,526.070740
|
||||
C748.184937,525.482117 748.129761,524.893555 748.074524,524.304932
|
||||
C743.625916,524.304932 739.177307,524.304932 734.787659,524.304932
|
||||
C734.573547,524.763489 734.342285,525.046631 734.316650,525.347290
|
||||
C734.105042,527.827087 734.009277,530.318909 733.728027,532.790039
|
||||
C733.313477,536.432983 734.160034,537.418396 737.825989,535.580933
|
||||
C743.170105,532.902283 746.194824,535.156616 745.891296,542.169128
|
||||
M788.188293,525.759277
|
||||
C784.717834,530.639099 784.656189,536.005798 786.150879,541.487305
|
||||
C787.114197,545.020081 789.453247,547.159302 793.298828,547.331177
|
||||
C797.185181,547.504883 799.986816,545.518921 800.774292,542.027039
|
||||
C801.591003,538.405762 801.956604,534.410889 801.318115,530.791321
|
||||
C800.091431,523.837769 795.222107,521.941406 788.188293,525.759277
|
||||
M834.572815,535.487549
|
||||
C834.175781,533.219666 834.462341,530.423096 833.216309,528.811829
|
||||
C831.489075,526.578247 828.606445,523.883728 826.225281,523.890198
|
||||
C823.831055,523.896729 821.012573,526.617676 819.209534,528.826233
|
||||
C818.030273,530.270691 818.069946,532.913330 818.009949,535.031982
|
||||
C817.791504,542.747192 820.623962,547.034851 825.870239,547.300293
|
||||
C831.111328,547.565491 833.492676,544.553772 834.572815,535.487549
|
||||
M652.416321,544.217712
|
||||
C650.345642,544.217712 648.274963,544.217712 646.204285,544.217712
|
||||
C646.838684,541.790405 647.850464,540.372437 649.179138,539.377747
|
||||
C651.032532,537.990356 653.289185,537.123291 655.073914,535.666748
|
||||
C657.983093,533.292542 658.921265,530.116333 656.902344,526.778687
|
||||
C654.763794,523.243286 651.190308,523.025208 647.608765,523.892029
|
||||
C644.105835,524.739807 641.985535,526.860413 642.362366,532.300232
|
||||
C644.056702,530.415955 644.736145,529.185913 645.777466,528.608765
|
||||
C647.529846,527.637512 649.488220,526.639526 651.407715,526.535706
|
||||
C652.368835,526.483582 654.366333,528.494934 654.258118,529.370850
|
||||
C654.041260,531.125488 652.986816,533.012512 651.738647,534.336182
|
||||
C650.537842,535.609619 648.631958,536.201843 647.078308,537.162109
|
||||
C643.543457,539.346802 641.603027,542.293274 642.488464,546.431335
|
||||
C647.810059,546.431335 652.869995,546.431335 657.929993,546.431335
|
||||
C657.979187,545.915588 658.028320,545.399841 658.077515,544.884094
|
||||
C656.491455,544.661987 654.905396,544.439941 652.416321,544.217712
|
||||
M665.208252,524.673828
|
||||
C659.711121,529.704834 660.278503,535.904724 662.117737,542.152100
|
||||
C663.162598,545.701355 666.081421,548.122620 669.655273,547.114319
|
||||
C672.280884,546.373535 675.324280,544.121033 676.420227,541.733643
|
||||
C677.700012,538.945862 677.519348,535.229553 677.167480,532.003906
|
||||
C676.408447,525.045532 672.824646,522.786072 665.208252,524.673828
|
||||
M695.703430,526.824768
|
||||
C692.833130,522.926819 688.881653,522.862183 684.933228,524.308411
|
||||
C682.220398,525.302063 680.293274,527.395935 681.342651,532.130493
|
||||
C682.861084,530.217041 683.638184,529.015991 684.630371,528.031738
|
||||
C686.979187,525.701782 690.038818,525.102783 691.995117,527.725464
|
||||
C692.869385,528.897522 691.875488,532.247437 690.726807,533.920532
|
||||
C689.607971,535.549927 687.186890,536.266479 685.376343,537.443420
|
||||
C682.032959,539.616821 680.202637,542.473877 681.275574,546.447998
|
||||
C686.507141,546.447998 691.585876,546.447998 696.664612,546.447998
|
||||
C696.714172,545.888977 696.763733,545.330017 696.813293,544.770996
|
||||
C692.817261,544.451782 688.821289,544.132629 684.825256,543.813416
|
||||
C684.609619,543.347107 684.393982,542.880859 684.178345,542.414551
|
||||
C686.441528,540.805054 688.587158,538.986511 690.994141,537.632751
|
||||
C695.067871,535.341797 697.437561,532.409607 695.703430,526.824768
|
||||
M609.871765,543.254517
|
||||
C612.920166,547.243774 616.962830,548.425964 620.513733,546.366577
|
||||
C623.687622,544.525879 625.242493,539.522339 623.739685,535.243408
|
||||
C622.335327,531.244812 619.265869,529.413025 615.161682,530.213440
|
||||
C610.440857,531.134216 608.941650,534.718079 609.006042,539.121460
|
||||
C609.022644,540.259460 609.365845,541.392761 609.871765,543.254517
|
||||
M598.632568,523.897034
|
||||
C596.280212,523.897034 593.927795,523.897034 591.575439,523.897034
|
||||
C591.513794,524.568726 591.452087,525.240417 591.390442,525.912109
|
||||
C593.818359,526.360107 596.246277,526.808167 598.972595,527.311218
|
||||
C598.972595,532.780090 598.878113,538.252136 599.051880,543.715759
|
||||
C599.086853,544.816223 600.139099,545.884399 600.722351,546.967468
|
||||
C601.157043,545.901306 601.943298,544.843018 601.967590,543.767578
|
||||
C602.093994,538.164673 602.026550,532.557373 602.026550,527.238770
|
||||
C604.818420,526.726013 606.992249,526.326721 609.166077,525.927429
|
||||
C609.066711,525.250671 608.967407,524.573853 608.868042,523.897095
|
||||
C605.769836,523.897095 602.671631,523.897095 598.632568,523.897034
|
||||
M809.033569,530.003784
|
||||
C809.069824,534.804382 809.030823,539.607727 809.209595,544.403015
|
||||
C809.242981,545.298035 810.150146,546.160522 810.653992,547.037964
|
||||
C811.315491,546.497864 812.542786,545.968018 812.552734,545.416138
|
||||
C812.682129,538.262817 812.638428,531.106323 812.638428,523.351135
|
||||
C809.349365,525.146240 807.111572,526.367615 803.909729,528.115112
|
||||
C806.222290,528.648987 807.433777,528.928711 809.033569,530.003784
|
||||
z"/>
|
||||
<path fill="#A8ADB0" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M848.743774,1439.752197
|
||||
C834.419617,1452.711060 814.984253,1454.616455 799.618713,1444.794189
|
||||
C784.187378,1434.929688 777.381104,1416.357056 782.851562,1399.040405
|
||||
C788.393555,1381.497681 804.724487,1369.880737 823.238037,1371.120239
|
||||
C839.614075,1372.216919 851.682190,1380.572754 857.870850,1395.782471
|
||||
C864.464417,1411.987305 861.071411,1426.778076 848.743774,1439.752197
|
||||
z"/>
|
||||
<path fill="#A8ADB0" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M827.000000,439.001709
|
||||
C812.017700,439.001678 797.535339,439.020386 783.053101,438.993744
|
||||
C775.748901,438.980286 771.116821,435.194092 771.012451,429.256409
|
||||
C770.903564,423.058716 775.608643,419.009918 783.040039,419.004883
|
||||
C808.342407,418.987823 833.644836,418.985809 858.947205,419.006348
|
||||
C866.245544,419.012268 870.884216,422.809875 870.987549,428.742432
|
||||
C871.095825,434.951721 866.404968,438.978607 858.961548,438.995575
|
||||
C848.474426,439.019501 837.987183,439.001678 827.000000,439.001709
|
||||
z"/>
|
||||
<path fill="#A5AAAD" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M725.800537,419.037415
|
||||
C732.110413,420.836304 735.046692,424.037842 734.979553,428.883942
|
||||
C734.912048,433.752899 732.381165,437.131927 727.861145,438.552002
|
||||
C723.255737,439.998871 719.365906,438.346191 716.640198,434.366302
|
||||
C712.145569,427.803436 716.655090,419.947510 725.800537,419.037415
|
||||
z"/>
|
||||
<path fill="#FFFFFF" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M1058.999390,738.000000
|
||||
C1058.999390,817.323120 1058.999512,896.146179 1058.998901,974.969299
|
||||
C1058.998901,986.083923 1056.137695,988.999451 1045.226807,988.999451
|
||||
C895.746216,989.000061 746.265686,989.000183 596.785095,988.999329
|
||||
C585.894104,988.999268 582.999939,986.054382 582.999878,974.984314
|
||||
C582.999512,855.666443 582.999451,736.348633 582.999939,617.030762
|
||||
C583.000000,605.768921 585.816956,603.000305 597.269653,603.000305
|
||||
C746.416931,603.000061 895.564270,603.000061 1044.711548,603.000366
|
||||
C1056.299927,603.000366 1058.999268,605.743347 1058.999268,617.515320
|
||||
C1058.999512,657.510193 1058.999390,697.505127 1058.999390,738.000000
|
||||
M624.634521,807.000000
|
||||
C615.001221,807.007446 615.001099,807.007446 615.000061,816.777527
|
||||
C614.999695,819.943726 614.998230,823.109863 615.000366,826.276062
|
||||
C615.005981,834.757751 615.240906,834.999023 623.686462,834.999146
|
||||
C737.835449,835.000488 851.984436,835.000000 966.133423,834.999634
|
||||
C967.299866,834.999634 968.467224,834.963196 969.632629,834.996582
|
||||
C973.453491,835.105896 975.055786,833.147644 975.007446,829.455505
|
||||
C974.948547,824.957031 975.000732,820.457092 974.999451,815.957825
|
||||
C974.996887,807.148193 974.851746,807.000610 966.046509,807.000549
|
||||
C852.564087,806.999634 739.081665,807.000000 624.634521,807.000000
|
||||
M615.000000,768.513550
|
||||
C615.000000,769.846680 614.999939,771.179749 615.000000,772.512878
|
||||
C615.000366,782.999634 615.000366,782.999939 625.422424,782.999939
|
||||
C738.569519,783.000000 851.716614,783.000000 964.863708,782.999756
|
||||
C966.363464,782.999756 967.865662,782.939331 969.362427,783.004944
|
||||
C973.425171,783.182983 975.099243,781.165955 975.017639,777.225525
|
||||
C974.924561,772.728271 975.001587,768.227478 974.999268,763.728271
|
||||
C974.994873,755.243896 974.757751,755.000977 966.317139,755.000854
|
||||
C853.336670,754.999512 740.356262,755.007507 627.375793,754.989807
|
||||
C614.303772,754.987732 614.895813,754.115295 615.000000,768.513550
|
||||
M972.647156,703.511108
|
||||
C970.337830,703.340759 968.028564,703.021851 965.719177,703.021362
|
||||
C851.890991,702.997009 738.062744,702.999939 624.234558,703.000366
|
||||
C622.901306,703.000427 621.567627,703.030029 620.234863,703.006165
|
||||
C616.713013,702.943176 615.004211,704.679993 615.009033,708.179688
|
||||
C615.015320,712.679443 615.000549,717.179260 615.000732,721.679016
|
||||
C615.001160,730.936829 615.060547,730.999451 624.091064,730.999512
|
||||
C738.085938,731.000305 852.080811,731.000000 966.075684,730.999634
|
||||
C967.242310,730.999634 968.409973,730.960449 969.575317,730.997681
|
||||
C973.368164,731.118652 975.041687,729.232483 975.006409,725.511902
|
||||
C974.952698,719.846008 975.103638,714.175598 974.918640,708.515320
|
||||
C974.868958,706.995239 973.917297,705.504639 972.647156,703.511108
|
||||
M790.500000,941.000000
|
||||
C807.829590,941.000000 825.159180,941.000122 842.488770,941.000000
|
||||
C874.981750,940.999878 907.474670,941.007996 939.967651,940.992310
|
||||
C946.113892,940.989380 946.948303,940.146790 946.993530,934.122131
|
||||
C947.024719,929.956604 947.002380,925.790649 946.999512,921.624878
|
||||
C946.993591,913.216492 946.777954,913.000916 938.214478,913.000793
|
||||
C833.403870,912.999634 728.593201,913.000000 623.782593,913.000183
|
||||
C622.782837,913.000183 621.780640,913.048218 620.783691,912.996277
|
||||
C616.810974,912.789429 614.915833,914.601990 614.985718,918.629822
|
||||
C615.069580,923.460571 614.993530,928.294006 615.002014,933.126282
|
||||
C615.015015,940.507080 615.506409,940.997253 623.036072,940.997803
|
||||
C678.524048,941.001953 734.012024,941.000000 790.500000,941.000000
|
||||
M905.500000,887.000000
|
||||
C917.330811,886.999023 929.162292,886.922363 940.992004,887.037537
|
||||
C945.216736,887.078613 947.143860,885.357971 947.025330,881.096191
|
||||
C946.900269,876.600708 947.002197,872.098938 946.999268,867.599915
|
||||
C946.993896,859.222290 946.770264,859.000916 938.189453,859.000793
|
||||
C833.378845,858.999634 728.568237,859.000000 623.757629,859.000122
|
||||
C622.757874,859.000122 621.755798,859.047729 620.758728,858.996765
|
||||
C616.771606,858.792786 614.911072,860.640869 614.985229,864.654785
|
||||
C615.071411,869.318848 614.997009,873.985718 615.001099,878.651367
|
||||
C615.008179,886.674133 615.336792,886.998840 623.560913,886.998962
|
||||
C717.207275,887.000610 810.853638,887.000000 905.500000,887.000000
|
||||
M649.500000,679.000000
|
||||
C670.492798,679.000000 691.485596,679.000122 712.478394,679.000061
|
||||
C744.300781,678.999939 776.123230,679.007507 807.945618,678.992737
|
||||
C813.972412,678.989929 814.922363,678.130737 814.986023,672.645508
|
||||
C815.044006,667.647888 815.030090,662.648865 814.990784,657.650879
|
||||
C814.945740,651.925720 814.056030,651.023560 808.241089,651.011108
|
||||
C793.412903,650.979370 778.584595,650.999939 763.756348,650.999939
|
||||
C716.605896,651.000000 669.455383,650.995667 622.304932,651.005432
|
||||
C615.932983,651.006775 615.060730,651.799377 615.010559,657.605591
|
||||
C614.965881,662.770142 615.112000,667.938354 614.969238,673.099304
|
||||
C614.850403,677.398254 616.822083,679.083801 621.009888,679.032410
|
||||
C630.171997,678.919861 639.336487,678.999084 649.500000,679.000000
|
||||
z"/>
|
||||
<path fill="#E9F1F7" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M838.538879,528.587646
|
||||
C841.257324,523.850464 846.111816,522.236023 850.432739,524.310608
|
||||
C854.543030,526.284058 855.016052,530.149902 851.688904,534.575378
|
||||
C855.691833,539.842896 856.009766,542.567871 852.937805,545.279297
|
||||
C849.674805,548.159302 842.052917,547.964355 839.096497,544.925293
|
||||
C836.324524,542.075928 836.744202,539.386841 840.461304,535.256653
|
||||
C839.750061,533.061462 839.083923,531.005493 838.538879,528.587646
|
||||
M851.140991,542.700439
|
||||
C850.508850,540.809998 850.249084,537.611389 849.166565,537.303345
|
||||
C846.959412,536.675110 844.192810,537.277588 841.911621,538.089417
|
||||
C841.221252,538.335144 840.763123,542.005554 841.238403,542.275269
|
||||
C843.326233,543.460266 845.761108,544.134338 848.145447,544.635925
|
||||
C848.876526,544.789795 849.847229,543.804626 851.140991,542.700439
|
||||
M847.170837,525.929871
|
||||
C845.322998,527.067627 843.475159,528.205322 841.627319,529.343018
|
||||
C842.556335,530.688049 843.409424,533.080505 844.430603,533.154541
|
||||
C846.401917,533.297424 848.468018,532.131409 850.494812,531.508057
|
||||
C849.658997,529.686768 848.823120,527.865540 847.170837,525.929871
|
||||
z"/>
|
||||
<path fill="#E7F0F6" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M746.012695,541.742188
|
||||
C746.194824,535.156616 743.170105,532.902283 737.825989,535.580933
|
||||
C734.160034,537.418396 733.313477,536.432983 733.728027,532.790039
|
||||
C734.009277,530.318909 734.105042,527.827087 734.316650,525.347290
|
||||
C734.342285,525.046631 734.573547,524.763489 734.787659,524.304932
|
||||
C739.177307,524.304932 743.625916,524.304932 748.074524,524.304932
|
||||
C748.129761,524.893555 748.184937,525.482117 748.240173,526.070740
|
||||
C744.746399,526.231140 741.252563,526.391479 737.205750,526.577209
|
||||
C737.066406,528.453491 736.926086,530.342651 736.826416,531.684448
|
||||
C740.397705,532.271240 743.774109,532.424500 746.814453,533.514832
|
||||
C748.107056,533.978394 749.089172,536.301086 749.525513,537.959473
|
||||
C750.549316,541.850952 748.078552,545.873596 744.370422,546.980408
|
||||
C739.339478,548.482056 734.869629,546.392883 732.290100,540.637451
|
||||
C737.081299,541.854736 741.158203,548.675415 746.012695,541.742188
|
||||
z"/>
|
||||
<path fill="#E3EDF5" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M788.456909,525.501526
|
||||
C795.222107,521.941406 800.091431,523.837769 801.318115,530.791321
|
||||
C801.956604,534.410889 801.591003,538.405762 800.774292,542.027039
|
||||
C799.986816,545.518921 797.185181,547.504883 793.298828,547.331177
|
||||
C789.453247,547.159302 787.114197,545.020081 786.150879,541.487305
|
||||
C784.656189,536.005798 784.717834,530.639099 788.456909,525.501526
|
||||
M788.678162,534.592041
|
||||
C788.826111,536.579346 788.469177,538.804993 789.263428,540.487183
|
||||
C790.065125,542.185181 791.953003,544.362366 793.467834,544.445007
|
||||
C794.903198,544.523438 797.529236,542.330444 797.772949,540.833740
|
||||
C798.372437,537.151489 798.224121,533.249084 797.689026,529.534241
|
||||
C797.503540,528.246399 795.248535,526.301636 794.018555,526.359314
|
||||
C792.454895,526.432556 790.637146,527.979858 789.565552,529.379211
|
||||
C788.767761,530.420837 788.981445,532.236938 788.678162,534.592041
|
||||
z"/>
|
||||
<path fill="#EBF2F8" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M717.505798,532.572144
|
||||
C724.369141,531.126282 728.479309,533.103760 729.059387,538.198303
|
||||
C729.526733,542.303223 728.052124,545.467590 724.034119,546.895996
|
||||
C719.206787,548.612000 714.843872,546.609497 711.383728,540.408997
|
||||
C713.459167,541.349426 714.735840,541.575317 715.479553,542.327942
|
||||
C718.204468,545.085571 721.385193,546.101440 724.214417,543.323975
|
||||
C725.444580,542.116272 725.929443,538.753906 725.141663,537.173096
|
||||
C723.437500,533.753479 720.182068,533.860352 717.021118,535.657959
|
||||
C713.919922,537.421631 712.744568,536.314331 713.093689,532.996155
|
||||
C713.389282,530.187317 713.709106,527.381104 714.034180,524.428162
|
||||
C718.655518,524.428162 723.196594,524.428162 727.737732,524.428162
|
||||
C727.765259,524.919922 727.792847,525.411682 727.820374,525.903442
|
||||
C725.398010,526.057617 722.955505,526.480469 720.558044,526.301270
|
||||
C717.218384,526.051575 715.969604,527.453918 716.169006,531.322876
|
||||
C716.577148,532.220520 717.041504,532.396301 717.505798,532.572144
|
||||
z"/>
|
||||
<path fill="#EBF2F8" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M758.505798,532.572144
|
||||
C765.369141,531.126282 769.479248,533.103760 770.059326,538.198303
|
||||
C770.526672,542.303223 769.052124,545.467529 765.034119,546.895874
|
||||
C760.206726,548.611938 755.843872,546.609497 752.383667,540.408936
|
||||
C754.459167,541.349365 755.735840,541.575317 756.479553,542.327881
|
||||
C759.204468,545.085571 762.385193,546.101440 765.214417,543.323975
|
||||
C766.444580,542.116272 766.929382,538.753906 766.141663,537.173096
|
||||
C764.437500,533.753479 761.182068,533.860352 758.021118,535.657959
|
||||
C754.919922,537.421631 753.744568,536.314331 754.093750,532.996155
|
||||
C754.389282,530.187317 754.709106,527.381104 755.034241,524.428162
|
||||
C759.655518,524.428162 764.196594,524.428162 768.737732,524.428162
|
||||
C768.765259,524.919922 768.792847,525.411682 768.820374,525.903442
|
||||
C766.398010,526.057617 763.955505,526.480469 761.558044,526.301270
|
||||
C758.218384,526.051575 756.969604,527.453918 757.169006,531.322876
|
||||
C757.577148,532.220459 758.041504,532.396301 758.505798,532.572144
|
||||
z"/>
|
||||
<path fill="#EBF2F8" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M834.566406,535.899048
|
||||
C833.492676,544.553772 831.111328,547.565491 825.870239,547.300293
|
||||
C820.623962,547.034851 817.791504,542.747192 818.009949,535.031982
|
||||
C818.069946,532.913330 818.030273,530.270691 819.209534,528.826233
|
||||
C821.012573,526.617676 823.831055,523.896729 826.225281,523.890198
|
||||
C828.606445,523.883728 831.489075,526.578247 833.216309,528.811829
|
||||
C834.462341,530.423096 834.175781,533.219666 834.566406,535.899048
|
||||
M821.967712,541.145508
|
||||
C823.533264,542.343506 824.987488,544.210571 826.704895,544.496399
|
||||
C827.789429,544.676880 830.234314,542.612671 830.387878,541.350769
|
||||
C830.857849,537.488831 830.876343,533.473267 830.294739,529.635010
|
||||
C830.088257,528.272278 827.649902,526.321838 826.236816,526.327271
|
||||
C824.812927,526.332703 822.430786,528.252625 822.168091,529.652649
|
||||
C821.515991,533.128479 821.832886,536.786072 821.967712,541.145508
|
||||
z"/>
|
||||
<path fill="#E4EEF5" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M652.867798,544.217773
|
||||
C654.905396,544.439941 656.491455,544.661987 658.077515,544.884094
|
||||
C658.028320,545.399841 657.979187,545.915588 657.929993,546.431335
|
||||
C652.869995,546.431335 647.810059,546.431335 642.488464,546.431335
|
||||
C641.603027,542.293274 643.543457,539.346802 647.078308,537.162109
|
||||
C648.631958,536.201843 650.537842,535.609619 651.738647,534.336182
|
||||
C652.986816,533.012512 654.041260,531.125488 654.258118,529.370850
|
||||
C654.366333,528.494934 652.368835,526.483582 651.407715,526.535706
|
||||
C649.488220,526.639526 647.529846,527.637512 645.777466,528.608765
|
||||
C644.736145,529.185913 644.056702,530.415955 642.362366,532.300232
|
||||
C641.985535,526.860413 644.105835,524.739807 647.608765,523.892029
|
||||
C651.190308,523.025208 654.763794,523.243286 656.902344,526.778687
|
||||
C658.921265,530.116333 657.983093,533.292542 655.073914,535.666748
|
||||
C653.289185,537.123291 651.032532,537.990356 649.179138,539.377747
|
||||
C647.850464,540.372437 646.838684,541.790405 646.204285,544.217712
|
||||
C648.274963,544.217712 650.345642,544.217712 652.867798,544.217773
|
||||
z"/>
|
||||
<path fill="#E8F1F7" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M665.513367,524.462524
|
||||
C672.824646,522.786072 676.408447,525.045532 677.167480,532.003906
|
||||
C677.519348,535.229553 677.700012,538.945862 676.420227,541.733643
|
||||
C675.324280,544.121033 672.280884,546.373535 669.655273,547.114319
|
||||
C666.081421,548.122620 663.162598,545.701355 662.117737,542.152100
|
||||
C660.278503,535.904724 659.711121,529.704834 665.513367,524.462524
|
||||
M664.593872,531.367371
|
||||
C664.728821,534.630981 664.454895,537.989563 665.180420,541.116089
|
||||
C665.516235,542.562927 667.937683,544.644775 669.168518,544.489990
|
||||
C670.818054,544.282593 673.140625,542.421326 673.547791,540.848938
|
||||
C674.311768,537.898865 674.443481,534.531616 673.804932,531.559937
|
||||
C673.371338,529.542358 671.176147,527.903442 669.768799,526.095154
|
||||
C668.109924,527.587708 666.451050,529.080261 664.593872,531.367371
|
||||
z"/>
|
||||
<path fill="#E7F0F6" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M695.860229,527.158691
|
||||
C697.437561,532.409607 695.067871,535.341797 690.994141,537.632751
|
||||
C688.587158,538.986511 686.441528,540.805054 684.178345,542.414551
|
||||
C684.393982,542.880859 684.609619,543.347107 684.825256,543.813416
|
||||
C688.821289,544.132629 692.817261,544.451782 696.813293,544.770996
|
||||
C696.763733,545.330017 696.714172,545.888977 696.664612,546.447998
|
||||
C691.585876,546.447998 686.507141,546.447998 681.275574,546.447998
|
||||
C680.202637,542.473877 682.032959,539.616821 685.376343,537.443420
|
||||
C687.186890,536.266479 689.607971,535.549927 690.726807,533.920532
|
||||
C691.875488,532.247437 692.869385,528.897522 691.995117,527.725464
|
||||
C690.038818,525.102783 686.979187,525.701782 684.630371,528.031738
|
||||
C683.638184,529.015991 682.861084,530.217041 681.342651,532.130493
|
||||
C680.293274,527.395935 682.220398,525.302063 684.933228,524.308411
|
||||
C688.881653,522.862183 692.833130,522.926819 695.860229,527.158691
|
||||
z"/>
|
||||
<path fill="#E8F1F7" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M609.715454,542.891357
|
||||
C609.365845,541.392761 609.022644,540.259460 609.006042,539.121460
|
||||
C608.941650,534.718079 610.440857,531.134216 615.161682,530.213440
|
||||
C619.265869,529.413025 622.335327,531.244812 623.739685,535.243408
|
||||
C625.242493,539.522339 623.687622,544.525879 620.513733,546.366577
|
||||
C616.962830,548.425964 612.920166,547.243774 609.715454,542.891357
|
||||
M612.838501,542.412964
|
||||
C614.474487,543.208008 616.369873,544.919189 617.675903,544.549316
|
||||
C619.078918,544.151855 620.587524,541.917725 620.880493,540.276306
|
||||
C621.466370,536.994324 621.517395,533.000000 616.877258,532.743835
|
||||
C612.226257,532.487183 612.023071,536.286316 612.082947,539.765015
|
||||
C612.094055,540.410278 612.346252,541.051453 612.838501,542.412964
|
||||
z"/>
|
||||
<path fill="#F1F6FA" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M599.103027,523.897095
|
||||
C602.671631,523.897095 605.769836,523.897095 608.868042,523.897095
|
||||
C608.967407,524.573853 609.066711,525.250671 609.166077,525.927429
|
||||
C606.992249,526.326721 604.818420,526.726013 602.026550,527.238770
|
||||
C602.026550,532.557373 602.093994,538.164673 601.967590,543.767578
|
||||
C601.943298,544.843018 601.157043,545.901306 600.722351,546.967468
|
||||
C600.139099,545.884399 599.086853,544.816223 599.051880,543.715759
|
||||
C598.878113,538.252136 598.972595,532.780090 598.972595,527.311218
|
||||
C596.246277,526.808167 593.818359,526.360107 591.390442,525.912109
|
||||
C591.452087,525.240417 591.513794,524.568726 591.575439,523.897034
|
||||
C593.927795,523.897034 596.280212,523.897034 599.103027,523.897095
|
||||
z"/>
|
||||
<path fill="#CADDEB" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M808.839417,529.606079
|
||||
C807.433777,528.928711 806.222290,528.648987 803.909729,528.115112
|
||||
C807.111572,526.367615 809.349365,525.146240 812.638428,523.351135
|
||||
C812.638428,531.106323 812.682129,538.262817 812.552734,545.416138
|
||||
C812.542786,545.968018 811.315491,546.497864 810.654053,547.037964
|
||||
C810.150146,546.160522 809.242981,545.298035 809.209595,544.403015
|
||||
C809.030823,539.607727 809.069824,534.804382 808.839417,529.606079
|
||||
z"/>
|
||||
<path fill="#A9AEB1" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M625.116882,807.000000
|
||||
C739.081665,807.000000 852.564087,806.999634 966.046509,807.000549
|
||||
C974.851746,807.000610 974.996887,807.148193 974.999451,815.957825
|
||||
C975.000732,820.457092 974.948547,824.957031 975.007446,829.455505
|
||||
C975.055786,833.147644 973.453491,835.105896 969.632629,834.996582
|
||||
C968.467224,834.963196 967.299866,834.999634 966.133423,834.999634
|
||||
C851.984436,835.000000 737.835449,835.000488 623.686462,834.999146
|
||||
C615.240906,834.999023 615.005981,834.757751 615.000366,826.276062
|
||||
C614.998230,823.109863 614.999695,819.943726 615.000061,816.777527
|
||||
C615.001099,807.007446 615.001221,807.007446 625.116882,807.000000
|
||||
z"/>
|
||||
<path fill="#A9AEB1" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M615.000000,768.024231
|
||||
C614.895813,754.115295 614.303772,754.987732 627.375793,754.989807
|
||||
C740.356262,755.007507 853.336670,754.999512 966.317139,755.000854
|
||||
C974.757751,755.000977 974.994873,755.243896 974.999268,763.728271
|
||||
C975.001587,768.227478 974.924561,772.728271 975.017639,777.225525
|
||||
C975.099243,781.165955 973.425171,783.182983 969.362427,783.004944
|
||||
C967.865662,782.939331 966.363464,782.999756 964.863708,782.999756
|
||||
C851.716614,783.000000 738.569519,783.000000 625.422424,782.999939
|
||||
C615.000366,782.999939 615.000366,782.999634 615.000000,772.512878
|
||||
C614.999939,771.179749 615.000000,769.846680 615.000000,768.024231
|
||||
z"/>
|
||||
<path fill="#A9AEB1" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M973.013428,703.755859
|
||||
C973.917297,705.504639 974.868958,706.995239 974.918640,708.515320
|
||||
C975.103638,714.175598 974.952698,719.846008 975.006409,725.511902
|
||||
C975.041687,729.232483 973.368164,731.118652 969.575317,730.997681
|
||||
C968.409973,730.960449 967.242310,730.999634 966.075684,730.999634
|
||||
C852.080811,731.000000 738.085938,731.000305 624.091064,730.999512
|
||||
C615.060547,730.999451 615.001160,730.936829 615.000732,721.679016
|
||||
C615.000549,717.179260 615.015320,712.679443 615.009033,708.179688
|
||||
C615.004211,704.679993 616.713013,702.943176 620.234863,703.006165
|
||||
C621.567627,703.030029 622.901306,703.000427 624.234558,703.000366
|
||||
C738.062744,702.999939 851.890991,702.997009 965.719177,703.021362
|
||||
C968.028564,703.021851 970.337830,703.340759 973.013428,703.755859
|
||||
z"/>
|
||||
<path fill="#A9AEB1" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M790.000000,941.000000
|
||||
C734.012024,941.000000 678.524048,941.001953 623.036072,940.997803
|
||||
C615.506409,940.997253 615.015015,940.507080 615.002014,933.126282
|
||||
C614.993530,928.294006 615.069580,923.460571 614.985718,918.629822
|
||||
C614.915833,914.601990 616.810974,912.789429 620.783691,912.996277
|
||||
C621.780640,913.048218 622.782837,913.000183 623.782593,913.000183
|
||||
C728.593201,913.000000 833.403870,912.999634 938.214478,913.000793
|
||||
C946.777954,913.000916 946.993591,913.216492 946.999512,921.624878
|
||||
C947.002380,925.790649 947.024719,929.956604 946.993530,934.122131
|
||||
C946.948303,940.146790 946.113892,940.989380 939.967651,940.992310
|
||||
C907.474670,941.007996 874.981750,940.999878 842.488770,941.000000
|
||||
C825.159180,941.000122 807.829590,941.000000 790.000000,941.000000
|
||||
z"/>
|
||||
<path fill="#A9AEB1" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M905.000000,887.000000
|
||||
C810.853638,887.000000 717.207275,887.000610 623.560913,886.998962
|
||||
C615.336792,886.998840 615.008179,886.674133 615.001099,878.651367
|
||||
C614.997009,873.985718 615.071411,869.318848 614.985229,864.654785
|
||||
C614.911072,860.640869 616.771606,858.792786 620.758728,858.996765
|
||||
C621.755798,859.047729 622.757874,859.000122 623.757629,859.000122
|
||||
C728.568237,859.000000 833.378845,858.999634 938.189453,859.000793
|
||||
C946.770264,859.000916 946.993896,859.222290 946.999268,867.599915
|
||||
C947.002197,872.098938 946.900269,876.600708 947.025330,881.096191
|
||||
C947.143860,885.357971 945.216736,887.078613 940.992004,887.037537
|
||||
C929.162292,886.922363 917.330811,886.999023 905.000000,887.000000
|
||||
z"/>
|
||||
<path fill="#A9AEB1" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M649.000000,679.000000
|
||||
C639.336487,678.999084 630.171997,678.919861 621.009888,679.032410
|
||||
C616.822083,679.083801 614.850403,677.398254 614.969238,673.099304
|
||||
C615.112000,667.938354 614.965881,662.770142 615.010559,657.605591
|
||||
C615.060730,651.799377 615.932983,651.006775 622.304932,651.005432
|
||||
C669.455383,650.995667 716.605896,651.000000 763.756348,650.999939
|
||||
C778.584595,650.999939 793.412903,650.979370 808.241089,651.011108
|
||||
C814.056030,651.023560 814.945740,651.925720 814.990784,657.650879
|
||||
C815.030090,662.648865 815.044006,667.647888 814.986023,672.645508
|
||||
C814.922363,678.130737 813.972412,678.989929 807.945618,678.992737
|
||||
C776.123230,679.007507 744.300781,678.999939 712.478394,679.000061
|
||||
C691.485596,679.000122 670.492798,679.000000 649.000000,679.000000
|
||||
z"/>
|
||||
<path fill="#1068A8" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M850.924561,543.020752
|
||||
C849.847229,543.804626 848.876526,544.789795 848.145447,544.635925
|
||||
C845.761108,544.134338 843.326233,543.460266 841.238403,542.275269
|
||||
C840.763123,542.005554 841.221252,538.335144 841.911621,538.089417
|
||||
C844.192810,537.277588 846.959412,536.675110 849.166565,537.303345
|
||||
C850.249084,537.611389 850.508850,540.809998 850.924561,543.020752
|
||||
z"/>
|
||||
<path fill="#1068A8" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M847.579102,525.987061
|
||||
C848.823120,527.865540 849.658997,529.686768 850.494812,531.508057
|
||||
C848.468018,532.131409 846.401917,533.297424 844.430603,533.154541
|
||||
C843.409424,533.080505 842.556335,530.688049 841.627319,529.343018
|
||||
C843.475159,528.205322 845.322998,527.067627 847.579102,525.987061
|
||||
z"/>
|
||||
<path fill="#0964A5" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M788.711426,534.150085
|
||||
C788.981445,532.236938 788.767761,530.420837 789.565552,529.379211
|
||||
C790.637146,527.979858 792.454895,526.432556 794.018555,526.359314
|
||||
C795.248535,526.301636 797.503540,528.246399 797.689026,529.534241
|
||||
C798.224121,533.249084 798.372437,537.151489 797.772949,540.833740
|
||||
C797.529236,542.330444 794.903198,544.523438 793.467834,544.445007
|
||||
C791.953003,544.362366 790.065125,542.185181 789.263428,540.487183
|
||||
C788.469177,538.804993 788.826111,536.579346 788.711426,534.150085
|
||||
z"/>
|
||||
<path fill="#005EA2" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M717.211609,532.294189
|
||||
C717.041504,532.396301 716.577148,532.220520 716.204712,531.770691
|
||||
C716.503540,531.669800 716.710449,531.842957 717.211609,532.294189
|
||||
z"/>
|
||||
<path fill="#005EA2" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M758.211609,532.294189
|
||||
C758.041504,532.396301 757.577148,532.220459 757.204712,531.770691
|
||||
C757.503540,531.669800 757.710449,531.842957 758.211609,532.294189
|
||||
z"/>
|
||||
<path fill="#0964A5" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M821.864136,540.758057
|
||||
C821.832886,536.786072 821.515991,533.128479 822.168091,529.652649
|
||||
C822.430786,528.252625 824.812927,526.332703 826.236816,526.327271
|
||||
C827.649902,526.321838 830.088257,528.272278 830.294739,529.635010
|
||||
C830.876343,533.473267 830.857849,537.488831 830.387878,541.350769
|
||||
C830.234314,542.612671 827.789429,544.676880 826.704895,544.496399
|
||||
C824.987488,544.210571 823.533264,542.343506 821.864136,540.758057
|
||||
z"/>
|
||||
<path fill="#0964A5" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M664.693054,530.970093
|
||||
C666.451050,529.080261 668.109924,527.587708 669.768799,526.095154
|
||||
C671.176147,527.903442 673.371338,529.542358 673.804932,531.559937
|
||||
C674.443481,534.531616 674.311768,537.898865 673.547791,540.848938
|
||||
C673.140625,542.421326 670.818054,544.282593 669.168518,544.489990
|
||||
C667.937683,544.644775 665.516235,542.562927 665.180420,541.116089
|
||||
C664.454895,537.989563 664.728821,534.630981 664.693054,530.970093
|
||||
z"/>
|
||||
<path fill="#0964A5" opacity="1.000000" stroke="none"
|
||||
d="
|
||||
M612.662842,542.053711
|
||||
C612.346252,541.051453 612.094055,540.410278 612.082947,539.765015
|
||||
C612.023071,536.286316 612.226257,532.487183 616.877258,532.743835
|
||||
C621.517395,533.000000 621.466370,536.994324 620.880493,540.276306
|
||||
C620.587524,541.917725 619.078918,544.151855 617.675903,544.549316
|
||||
C616.369873,544.919189 614.474487,543.208008 612.662842,542.053711
|
||||
z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 40 KiB |
300
app/assets/javascripts/activityChart.js
Normal file
300
app/assets/javascripts/activityChart.js
Normal file
@@ -0,0 +1,300 @@
|
||||
(function (window) {
|
||||
|
||||
if (document.getElementById('activityChartContainer')) {
|
||||
|
||||
const COLORS = {
|
||||
delivered: '#0076d6',
|
||||
failed: '#fa9441',
|
||||
text: '#666'
|
||||
};
|
||||
|
||||
const FONT_SIZE = 16;
|
||||
const FONT_WEIGHT = 'bold';
|
||||
const MAX_Y = 120;
|
||||
|
||||
const createChart = function(containerId, labels, deliveredData, failedData) {
|
||||
const container = d3.select(containerId);
|
||||
container.selectAll('*').remove(); // Clear any existing content
|
||||
|
||||
const margin = { top: 60, right: 20, bottom: 40, left: 20 }; // Adjusted top margin for legend
|
||||
const width = container.node().getBoundingClientRect().width - margin.left - margin.right;
|
||||
const height = 400 - margin.top - margin.bottom;
|
||||
|
||||
const svg = container.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
let tooltip = d3.select('#tooltip');
|
||||
|
||||
if (tooltip.empty()) {
|
||||
tooltip = d3.select('body').append('div')
|
||||
.attr('id', 'tooltip')
|
||||
.style('display', 'none');
|
||||
}
|
||||
// Create legend
|
||||
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 }
|
||||
];
|
||||
|
||||
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('span')
|
||||
.attr('class', 'legend-label')
|
||||
.text(d => d.label);
|
||||
|
||||
const x = d3.scaleBand()
|
||||
.domain(labels)
|
||||
.range([0, width])
|
||||
.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.scaleSqrt()
|
||||
.domain([0, maxY + 2]) // Add 2 units of space at the top
|
||||
.nice()
|
||||
.range([height, 0]);
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'x axis')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x));
|
||||
|
||||
// Generate the y-axis with whole numbers
|
||||
const yAxis = d3.axisLeft(y)
|
||||
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data
|
||||
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
|
||||
|
||||
svg.append('g')
|
||||
.attr('class', 'y axis')
|
||||
.call(yAxis);
|
||||
|
||||
// Data for stacking
|
||||
const stackData = labels.map((label, i) => ({
|
||||
label: label,
|
||||
delivered: deliveredData[i],
|
||||
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
|
||||
}));
|
||||
|
||||
// Stack the data
|
||||
const stack = d3.stack()
|
||||
.keys(['delivered', 'failed'])
|
||||
.order(d3.stackOrderNone)
|
||||
.offset(d3.stackOffsetNone);
|
||||
|
||||
const series = stack(stackData);
|
||||
|
||||
// Color scale
|
||||
const color = d3.scaleOrdinal()
|
||||
.domain(['delivered', 'failed'])
|
||||
.range([COLORS.delivered, COLORS.failed]);
|
||||
|
||||
// Create bars with animation
|
||||
const barGroups = svg.selectAll('.bar-group')
|
||||
.data(series)
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('class', 'bar-group')
|
||||
.attr('fill', d => color(d.key));
|
||||
|
||||
barGroups.selectAll('rect')
|
||||
.data(d => d)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', d => x(d.data.label))
|
||||
.attr('y', height)
|
||||
.attr('height', 0)
|
||||
.attr('width', x.bandwidth())
|
||||
.on('mouseover', function(event, d) {
|
||||
const key = d3.select(this.parentNode).datum().key;
|
||||
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
tooltip.style('display', 'block')
|
||||
.html(`${d.data.label}<br>${capitalizedKey}: ${d.data[key]}`);
|
||||
})
|
||||
.on('mousemove', function(event) {
|
||||
tooltip.style('left', `${event.pageX + 10}px`)
|
||||
.style('top', `${event.pageY - 20}px`);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
tooltip.style('display', 'none');
|
||||
})
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.attr('y', d => y(d[1]))
|
||||
.attr('height', d => y(d[0]) - y(d[1]));
|
||||
};
|
||||
|
||||
// Function to create an accessible table
|
||||
const createTable = function(tableId, chartType, labels, deliveredData, failedData) {
|
||||
const table = document.getElementById(tableId);
|
||||
table.innerHTML = ""; // Clear previous data
|
||||
|
||||
const captionText = document.querySelector(`#${chartType} .chart-subtitle`).textContent;
|
||||
const caption = document.createElement('caption');
|
||||
caption.textContent = captionText;
|
||||
const thead = document.createElement('thead');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
// Create table header
|
||||
const headerRow = document.createElement('tr');
|
||||
const headers = ['Day', 'Delivered', 'Failed'];
|
||||
headers.forEach(headerText => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = headerText;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
thead.appendChild(headerRow);
|
||||
|
||||
// Create table body
|
||||
labels.forEach((label, index) => {
|
||||
const row = document.createElement('tr');
|
||||
const cellDay = document.createElement('td');
|
||||
cellDay.textContent = label;
|
||||
row.appendChild(cellDay);
|
||||
|
||||
const cellDelivered = document.createElement('td');
|
||||
cellDelivered.textContent = deliveredData[index];
|
||||
row.appendChild(cellDelivered);
|
||||
|
||||
const cellFailed = document.createElement('td');
|
||||
cellFailed.textContent = failedData[index];
|
||||
row.appendChild(cellFailed);
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
table.appendChild(caption);
|
||||
table.appendChild(thead);
|
||||
table.append(tbody);
|
||||
};
|
||||
|
||||
const fetchData = function(type) {
|
||||
var ctx = document.getElementById('weeklyChart');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
labels = [];
|
||||
deliveredData = [];
|
||||
failedData = [];
|
||||
|
||||
let totalMessages = 0;
|
||||
|
||||
for (var dateString in data) {
|
||||
if (data.hasOwnProperty(dateString)) {
|
||||
const dateParts = dateString.split('-');
|
||||
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
};
|
||||
|
||||
const handleDropdownChange = function(event) {
|
||||
const selectedValue = event.target.value;
|
||||
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
|
||||
const selectElement = document.getElementById('options');
|
||||
const selectedText = selectElement.options[selectElement.selectedIndex].text;
|
||||
|
||||
subTitle.textContent = `${selectedText} - last 7 days`;
|
||||
fetchData(selectedValue);
|
||||
|
||||
// Update ARIA live region
|
||||
const liveRegion = document.getElementById('aria-live-account');
|
||||
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
|
||||
|
||||
// Switch tables based on dropdown selection
|
||||
const selectedTable = selectedValue === "individual" ? "table1" : "table2";
|
||||
const tables = document.querySelectorAll('.table-overflow-x-auto');
|
||||
tables.forEach(function(table) {
|
||||
table.classList.add('hidden'); // Hide all tables by adding the hidden class
|
||||
table.classList.remove('visible'); // Ensure they are not visible
|
||||
});
|
||||
const tableToShow = document.getElementById(selectedTable);
|
||||
tableToShow.classList.remove('hidden'); // Remove hidden class
|
||||
tableToShow.classList.add('visible'); // Add visible class
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize activityChart chart and table with service data by default
|
||||
fetchData('service');
|
||||
|
||||
// Add event listener to the dropdown
|
||||
const dropdown = document.getElementById('options');
|
||||
dropdown.addEventListener('change', handleDropdownChange);
|
||||
});
|
||||
|
||||
// Resize chart on window resize
|
||||
window.addEventListener('resize', function() {
|
||||
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0) {
|
||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||
}
|
||||
});
|
||||
|
||||
// Export functions for testing
|
||||
window.createChart = createChart;
|
||||
window.createTable = createTable;
|
||||
window.handleDropdownChange = handleDropdownChange;
|
||||
window.fetchData = fetchData;
|
||||
}
|
||||
|
||||
})(window);
|
||||
@@ -1,67 +0,0 @@
|
||||
(function (window) {
|
||||
|
||||
function initializeChartAndSocket() {
|
||||
var ctx = document.getElementById('myChart');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
var myBarChart = new Chart(ctx.getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Delivered',
|
||||
data: [],
|
||||
backgroundColor: '#0076d6',
|
||||
stack: 'Stack 0'
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var socket = io();
|
||||
|
||||
socket.on('connect', function() {
|
||||
socket.emit('fetch_daily_stats_by_user');
|
||||
});
|
||||
|
||||
socket.on('daily_stats_by_user_update', function(data) {
|
||||
// console.log('Data received:', data);
|
||||
var labels = [];
|
||||
var deliveredData = [];
|
||||
|
||||
for (var date in data) {
|
||||
labels.push(date);
|
||||
deliveredData.push(data[date].sms.delivered);
|
||||
}
|
||||
|
||||
myBarChart.data.labels = labels;
|
||||
myBarChart.data.datasets[0].data = deliveredData;
|
||||
myBarChart.update();
|
||||
});
|
||||
|
||||
socket.on('error', function(data) {
|
||||
// console.log('Error:', data);
|
||||
});
|
||||
|
||||
var sevenDaysButton = document.getElementById('sevenDaysButton');
|
||||
if (sevenDaysButton) {
|
||||
sevenDaysButton.addEventListener('click', function() {
|
||||
socket.emit('fetch_daily_stats_by_user');
|
||||
// console.log('clicked');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializeChartAndSocket);
|
||||
|
||||
})(window);
|
||||
150
app/assets/javascripts/totalMessagesChart.js
Normal file
150
app/assets/javascripts/totalMessagesChart.js
Normal file
@@ -0,0 +1,150 @@
|
||||
(function (window) {
|
||||
function createTotalMessagesChart() {
|
||||
var chartContainer = document.getElementById('totalMessageChartContainer');
|
||||
if (!chartContainer) return;
|
||||
|
||||
var chartTitle = document.getElementById('chartTitle').textContent;
|
||||
|
||||
// Access data attributes from the HTML
|
||||
var sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
|
||||
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
|
||||
var totalMessages = sms_sent + sms_remaining_messages;
|
||||
|
||||
// Update the message below the chart
|
||||
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 = (sms_sent === 0) ? 0 : 0.02;
|
||||
var minSentValue = totalMessages * minSentPercentage;
|
||||
var displaySent = Math.max(sms_sent, minSentValue);
|
||||
var displayRemaining = totalMessages - displaySent;
|
||||
|
||||
var svg = d3.select("#totalMessageChart");
|
||||
var width = chartContainer.clientWidth;
|
||||
var height = 48;
|
||||
|
||||
// Ensure the width is set correctly
|
||||
if (width === 0) {
|
||||
console.error('Chart container width is 0, cannot set SVG width.');
|
||||
return;
|
||||
}
|
||||
|
||||
svg.attr("width", width).attr("height", height);
|
||||
|
||||
var x = d3.scaleLinear()
|
||||
.domain([0, totalMessages])
|
||||
.range([0, width]);
|
||||
|
||||
// Create tooltip dynamically
|
||||
var tooltip = d3.select("body").append("div")
|
||||
.attr("id", "tooltip");
|
||||
|
||||
// Create the initial bars
|
||||
var sentBar = svg.append("rect")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("height", height)
|
||||
.attr("fill", '#0076d6')
|
||||
.attr("width", 0) // Start with width 0 for animation
|
||||
.on('mouseover', function(event) {
|
||||
tooltip.style('display', 'block')
|
||||
.html(`Messages Sent: ${sms_sent.toLocaleString()}`);
|
||||
})
|
||||
.on('mousemove', function(event) {
|
||||
tooltip.style('left', `${event.pageX + 10}px`)
|
||||
.style('top', `${event.pageY - 20}px`);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
tooltip.style('display', 'none');
|
||||
});
|
||||
|
||||
var remainingBar = svg.append("rect")
|
||||
.attr("x", 0) // Initially set to 0, will be updated during animation
|
||||
.attr("y", 0)
|
||||
.attr("height", height)
|
||||
.attr("fill", '#C7CACE')
|
||||
.attr("width", 0) // Start with width 0 for animation
|
||||
.on('mouseover', function(event) {
|
||||
tooltip.style('display', 'block')
|
||||
.html(`Remaining: ${sms_remaining_messages.toLocaleString()}`);
|
||||
})
|
||||
.on('mousemove', function(event) {
|
||||
tooltip.style('left', `${event.pageX + 10}px`)
|
||||
.style('top', `${event.pageY - 20}px`);
|
||||
})
|
||||
.on('mouseout', function() {
|
||||
tooltip.style('display', 'none');
|
||||
});
|
||||
|
||||
// Animate the bars together as a single cohesive line
|
||||
svg.transition()
|
||||
.duration(1000) // Total animation duration
|
||||
.attr("width", width)
|
||||
.tween("resize", function() {
|
||||
var interpolator = d3.interpolate(0, width);
|
||||
return function(t) {
|
||||
var newWidth = interpolator(t);
|
||||
var sentWidth = x(displaySent) / width * newWidth;
|
||||
var remainingWidth = x(displayRemaining) / width * newWidth;
|
||||
sentBar.attr("width", sentWidth);
|
||||
remainingBar.attr("x", sentWidth).attr("width", remainingWidth);
|
||||
};
|
||||
});
|
||||
|
||||
// Create and populate the accessible table
|
||||
var tableContainer = document.getElementById('totalMessageTable');
|
||||
var table = document.createElement('table');
|
||||
table.className = 'usa-sr-only usa-table';
|
||||
|
||||
var caption = document.createElement('caption');
|
||||
caption.textContent = chartTitle;
|
||||
table.appendChild(caption);
|
||||
|
||||
var thead = document.createElement('thead'); // Ensure thead is created
|
||||
var theadRow = document.createElement('tr');
|
||||
var thMessagesSent = document.createElement('th');
|
||||
thMessagesSent.textContent = 'Messages Sent'; // First column header
|
||||
var thRemaining = document.createElement('th');
|
||||
thRemaining.textContent = 'Remaining'; // Second column header
|
||||
theadRow.appendChild(thMessagesSent);
|
||||
theadRow.appendChild(thRemaining);
|
||||
thead.appendChild(theadRow); // Append theadRow to the thead
|
||||
table.appendChild(thead);
|
||||
|
||||
var tbody = document.createElement('tbody');
|
||||
var tbodyRow = document.createElement('tr');
|
||||
|
||||
var tdMessagesSent = document.createElement('td');
|
||||
tdMessagesSent.textContent = sms_sent.toLocaleString(); // Value for Messages Sent
|
||||
var tdRemaining = document.createElement('td');
|
||||
tdRemaining.textContent = sms_remaining_messages.toLocaleString(); // Value for Remaining
|
||||
|
||||
tbodyRow.appendChild(tdMessagesSent);
|
||||
tbodyRow.appendChild(tdRemaining);
|
||||
tbody.appendChild(tbodyRow);
|
||||
|
||||
table.appendChild(tbody);
|
||||
tableContainer.appendChild(table);
|
||||
|
||||
table.appendChild(tbody);
|
||||
tableContainer.appendChild(table);
|
||||
|
||||
// Ensure the chart resizes correctly on window resize
|
||||
window.addEventListener('resize', function () {
|
||||
width = chartContainer.clientWidth;
|
||||
x.range([0, width]);
|
||||
svg.attr("width", width);
|
||||
sentBar.attr("width", x(displaySent));
|
||||
remainingBar.attr("x", x(displaySent)).attr("width", x(displayRemaining));
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize total messages chart if the container exists
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
createTotalMessagesChart();
|
||||
});
|
||||
|
||||
// Export function for testing
|
||||
window.createTotalMessagesChart = createTotalMessagesChart;
|
||||
|
||||
})(window);
|
||||
4
app/assets/js/setTimezone.js
Normal file
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=/`;
|
||||
})
|
||||
159
app/assets/sass/uswds/_data-visualization.scss
Normal file
159
app/assets/sass/uswds/_data-visualization.scss
Normal file
@@ -0,0 +1,159 @@
|
||||
@use "uswds-core" as *;
|
||||
|
||||
$delivered: color('blue-50v');
|
||||
$pending: color('green-cool-40v');
|
||||
$failed: color('gray-cool-20');
|
||||
|
||||
.chart-container {
|
||||
display: flex;
|
||||
&.usage {
|
||||
height: units(4);
|
||||
}
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
#totalMessageChartContainer {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
border-radius: units(0.5);
|
||||
&.delivered, &.usage {
|
||||
background-color: $delivered;
|
||||
margin-right: 1px;
|
||||
}
|
||||
&.pending{
|
||||
background-color: $pending;
|
||||
margin-right: 1px;
|
||||
}
|
||||
&.failed, &.remaining {
|
||||
background-color: $failed;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
margin: units(1) 0;
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-right: units(2);
|
||||
}
|
||||
.legend-color {
|
||||
width: units(3);
|
||||
height: units(3);
|
||||
margin-right: 0;
|
||||
padding: 0;
|
||||
border-radius: 2px;
|
||||
background-color: $delivered;
|
||||
&.pending {
|
||||
background-color: $pending;
|
||||
}
|
||||
&.failed, &.remaining {
|
||||
background-color: $failed;
|
||||
}
|
||||
}
|
||||
.legend-value {
|
||||
margin: 0 units(1);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-tooltip {
|
||||
line-height: 1;
|
||||
.usa-tooltip__body {
|
||||
width: units(mobile);
|
||||
font-size: size("body", 2);
|
||||
height: auto;
|
||||
white-space: wrap;
|
||||
line-height: units(1);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 300px;
|
||||
height: 20px;
|
||||
background-color: #eee;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-bar-inner {
|
||||
height: 100%;
|
||||
background-color: #007bff;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: units(1) units(1) 0;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
font-size: size("body", 6);
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.axis text {
|
||||
font-size: size("body", 1);
|
||||
}
|
||||
|
||||
.axis line,
|
||||
.axis path {
|
||||
shape-rendering: crispEdges;
|
||||
stroke: #000;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.bar {
|
||||
fill-opacity: 0.8;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: units(2);
|
||||
.legend-rect {
|
||||
width: units(2);
|
||||
height: units(2);
|
||||
margin-right: units(1);
|
||||
}
|
||||
}
|
||||
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
background: color('ink');
|
||||
color: #FFF;
|
||||
border: 1px solid #ccc;
|
||||
padding: units(1);
|
||||
border-radius: units(1);
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
font-size: size("body", 3);
|
||||
line-height: 1.3;
|
||||
}
|
||||
@@ -344,11 +344,3 @@ h2.recipient-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.js-focus-style {
|
||||
outline: 3px solid color("blue-40v");
|
||||
box-shadow: 0 0 0 7px color("blue-40v");
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ i.e.
|
||||
|
||||
@use "uswds-core" as *;
|
||||
|
||||
iframe:focus, [href]:focus, [tabindex]:focus, [contentEditable=true]:focus {
|
||||
outline: units(1px) dotted color('blue-40v');
|
||||
outline: units(1px) auto color('blue-40v');
|
||||
iframe:focus, [href]:focus, [tabindex]:focus, [contentEditable=true]:focus, button:not([disabled]):focus {
|
||||
outline: units(2px) solid color('blue-40v');
|
||||
outline-offset: 0.3rem;
|
||||
}
|
||||
|
||||
@@ -277,6 +276,12 @@ td.table-empty-message {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.usa-table {
|
||||
th {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.js-stick-at-bottom-when-scrolling {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -327,6 +332,22 @@ td.table-empty-message {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.table-overflow-x-auto {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: units('desktop-lg')) {
|
||||
.table-overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
table {
|
||||
min-width: 768px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Dashboard
|
||||
|
||||
.dashboard {
|
||||
@@ -376,9 +397,6 @@ td.table-empty-message {
|
||||
background-image: url(../img/material-icons/description.svg);
|
||||
}
|
||||
}
|
||||
.table-wrapper {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
@@ -402,6 +420,30 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.job-status-table {
|
||||
table-layout: fixed;
|
||||
|
||||
thead tr th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
th:nth-child(2),R
|
||||
td:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.usage-table {
|
||||
ul {
|
||||
list-style: none;
|
||||
@@ -421,27 +463,51 @@ td.table-empty-message {
|
||||
width: 25%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
td.jobid {
|
||||
width: 5%;
|
||||
}
|
||||
td.template {
|
||||
width: 20%;
|
||||
width: 25%;
|
||||
}
|
||||
td.time-sent {
|
||||
width: 20%;
|
||||
width: 15%;
|
||||
}
|
||||
td.sender {
|
||||
width: 15%;
|
||||
width: 20%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
td.count-of-recipients {
|
||||
width: 5%;
|
||||
}
|
||||
td.report {
|
||||
width: 5%;
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.delivered {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.failed {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.report img {
|
||||
padding-top: 5px;
|
||||
}
|
||||
th {
|
||||
padding: 0.5rem 1rem
|
||||
}
|
||||
td {
|
||||
padding: 0.5rem 1rem
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.usa-table-container--scrollable-mobile {
|
||||
margin: 0;
|
||||
overflow-y:hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-table th[data-sortable][aria-sort=ascending], .usa-table th[data-sortable][aria-sort=descending] {
|
||||
background-color: #a1d3ff;
|
||||
}
|
||||
|
||||
#template-list {
|
||||
@@ -456,6 +522,10 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.usa-prose > p.max-width-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// Tabs
|
||||
|
||||
.tabs {
|
||||
|
||||
@@ -13,5 +13,6 @@ in the form $setting: value,
|
||||
$theme-banner-max-width: "desktop-lg",
|
||||
$theme-grid-container-max-width: "desktop-lg",
|
||||
$theme-footer-max-width: "desktop-lg",
|
||||
$theme-header-max-width: "desktop-lg"
|
||||
$theme-header-max-width: "desktop-lg",
|
||||
$theme-identifier-max-width: "desktop-lg"
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@forward "uswds-theme";
|
||||
@forward "uswds";
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@forward "legacy-styles";
|
||||
@forward "legacy-styles";
|
||||
@forward "data-visualization";
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -231,6 +231,11 @@ def naturaltime_without_indefinite_article(date):
|
||||
)
|
||||
|
||||
|
||||
def convert_time_unixtimestamp(date_string):
|
||||
dt = datetime.fromisoformat(date_string)
|
||||
return int(dt.timestamp())
|
||||
|
||||
|
||||
def format_delta(date):
|
||||
# This method assumes that date is in UTC
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
@@ -3,6 +3,7 @@ from flask import Blueprint
|
||||
main = Blueprint("main", __name__)
|
||||
|
||||
from app.main.views import ( # noqa isort:skip
|
||||
activity,
|
||||
add_service,
|
||||
api_keys,
|
||||
choose_account,
|
||||
|
||||
@@ -50,6 +50,7 @@ from app.main.validators import (
|
||||
CommonlyUsedPassword,
|
||||
CsvFileValidator,
|
||||
DoesNotStartWithDoubleZero,
|
||||
FieldCannotContainComma,
|
||||
LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly,
|
||||
MustContainAlphanumericCharacters,
|
||||
NoCommasInPlaceHolders,
|
||||
@@ -1650,7 +1651,11 @@ def get_placeholder_form_instance(
|
||||
) # TODO: replace with us_mobile_number
|
||||
else:
|
||||
field = GovukTextInputField(
|
||||
placeholder_name, validators=[DataRequired(message="Cannot be empty")]
|
||||
placeholder_name,
|
||||
validators=[
|
||||
DataRequired(message="Cannot be empty"),
|
||||
FieldCannotContainComma(),
|
||||
],
|
||||
)
|
||||
|
||||
PlaceholderForm.placeholder_value = field
|
||||
|
||||
@@ -161,6 +161,15 @@ class DoesNotStartWithDoubleZero:
|
||||
raise ValidationError(self.message)
|
||||
|
||||
|
||||
class FieldCannotContainComma:
|
||||
def __init__(self, message="Cannot contain a comma"):
|
||||
self.message = message
|
||||
|
||||
def __call__(self, form, field):
|
||||
if field.data and "," in field.data:
|
||||
raise ValidationError(self.message)
|
||||
|
||||
|
||||
class MustContainAlphanumericCharacters:
|
||||
regex = re.compile(r".*[a-zA-Z0-9].*[a-zA-Z0-9].*")
|
||||
|
||||
|
||||
124
app/main/views/activity.py
Normal file
124
app/main/views/activity.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from flask import abort, render_template, request, url_for
|
||||
|
||||
from app import current_service, job_api_client
|
||||
from app.formatters import convert_time_unixtimestamp, get_time_left
|
||||
from app.main import main
|
||||
from app.utils.pagination import (
|
||||
generate_next_dict,
|
||||
generate_pagination_pages,
|
||||
generate_previous_dict,
|
||||
get_page_from_request,
|
||||
)
|
||||
from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/activity/services/<uuid:service_id>")
|
||||
@user_has_permissions("view_activity")
|
||||
def all_jobs_activity(service_id):
|
||||
service_data_retention_days = 7
|
||||
page = get_page_from_request()
|
||||
jobs = job_api_client.get_page_of_jobs(service_id, page=page)
|
||||
all_jobs_dict = generate_job_dict(jobs)
|
||||
prev_page, next_page, pagination = handle_pagination(jobs, service_id, page)
|
||||
message_type = ("sms",)
|
||||
return render_template(
|
||||
"views/activity/all-activity.html",
|
||||
all_jobs_dict=all_jobs_dict,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
next_page=next_page,
|
||||
prev_page=prev_page,
|
||||
pagination=pagination,
|
||||
download_link_one_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="one_day",
|
||||
),
|
||||
download_link_three_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="three_day",
|
||||
),
|
||||
download_link_five_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="five_day",
|
||||
),
|
||||
download_link_seven_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="seven_day",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def handle_pagination(jobs, service_id, page):
|
||||
if page is None:
|
||||
abort(404, "Invalid page argument ({}).".format(request.args.get("page")))
|
||||
prev_page = (
|
||||
generate_previous_dict("main.all_jobs_activity", service_id, page)
|
||||
if page > 1
|
||||
else None
|
||||
)
|
||||
next_page = (
|
||||
generate_next_dict("main.all_jobs_activity", service_id, page)
|
||||
if jobs.get("links", {}).get("next")
|
||||
else None
|
||||
)
|
||||
pagination = generate_pagination_pages(
|
||||
jobs.get("total", {}), jobs.get("page_size", {}), page
|
||||
)
|
||||
return prev_page, next_page, pagination
|
||||
|
||||
|
||||
def generate_job_dict(jobs):
|
||||
return [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
"download_link": url_for(
|
||||
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"view_job_link": url_for(
|
||||
".view_job", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"created_at": job["created_at"],
|
||||
"time_sent_data_value": convert_time_unixtimestamp(
|
||||
job["processing_finished"]
|
||||
if job["processing_finished"]
|
||||
else (
|
||||
job["processing_started"]
|
||||
if job["processing_started"]
|
||||
else job["created_at"]
|
||||
)
|
||||
),
|
||||
"processing_finished": job["processing_finished"],
|
||||
"processing_started": job["processing_started"],
|
||||
"created_by": job["created_by"],
|
||||
"template_name": job["template_name"],
|
||||
"delivered_count": next(
|
||||
(
|
||||
stat["count"]
|
||||
for stat in job.get("statistics", [])
|
||||
if stat["status"] == "delivered"
|
||||
),
|
||||
None,
|
||||
),
|
||||
"failed_count": next(
|
||||
(
|
||||
stat["count"]
|
||||
for stat in job.get("statistics", [])
|
||||
if stat["status"] == "failed"
|
||||
),
|
||||
None,
|
||||
),
|
||||
}
|
||||
for job in jobs["data"]
|
||||
]
|
||||
@@ -1,25 +1,22 @@
|
||||
import calendar
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
|
||||
from flask import Response, abort, jsonify, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from flask_socketio import emit
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from app import (
|
||||
billing_api_client,
|
||||
current_service,
|
||||
job_api_client,
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
socketio,
|
||||
template_statistics_client,
|
||||
)
|
||||
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,
|
||||
@@ -34,38 +31,6 @@ from app.utils.user import user_has_permissions
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
|
||||
|
||||
@socketio.on("fetch_daily_stats")
|
||||
def handle_fetch_daily_stats():
|
||||
service_id = session.get("service_id")
|
||||
if service_id:
|
||||
date_range = get_stats_date_range()
|
||||
daily_stats = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||
)
|
||||
emit("daily_stats_update", daily_stats)
|
||||
else:
|
||||
emit("error", {"error": "No service_id provided"})
|
||||
|
||||
|
||||
@socketio.on("fetch_daily_stats_by_user")
|
||||
def handle_fetch_daily_stats_by_user():
|
||||
service_id = session.get("service_id")
|
||||
user_id = session.get("user_id")
|
||||
if service_id and user_id:
|
||||
date_range = get_stats_date_range()
|
||||
daily_stats_by_user = (
|
||||
service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
user_id,
|
||||
start_date=date_range["start_date"],
|
||||
days=date_range["days"],
|
||||
)
|
||||
)
|
||||
emit("daily_stats_by_user_update", daily_stats_by_user)
|
||||
else:
|
||||
emit("error", {"error": "No service_id or user_id provided"})
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard")
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
def old_service_dashboard(service_id):
|
||||
@@ -75,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
|
||||
@@ -82,19 +48,21 @@ def service_dashboard(service_id):
|
||||
if not current_user.has_permissions("view_activity"):
|
||||
return redirect(url_for("main.choose_template", service_id=service_id))
|
||||
|
||||
yearly_usage = billing_api_client.get_annual_usage_for_service(
|
||||
service_id,
|
||||
get_current_financial_year(),
|
||||
)
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
current_service.id,
|
||||
)
|
||||
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
|
||||
sms_sent = usage_data["sms_sent"]
|
||||
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
|
||||
|
||||
job_response = job_api_client.get_jobs(service_id)["data"]
|
||||
notifications_response = notification_api_client.get_notifications_for_service(
|
||||
service_id
|
||||
)["notifications"]
|
||||
service_data_retention_days = 7
|
||||
|
||||
aggregate_notifications_by_job = defaultdict(list)
|
||||
for notification in notifications_response:
|
||||
job_id = notification.get("job", {}).get("id", None)
|
||||
if job_id:
|
||||
aggregate_notifications_by_job[job_id].append(notification)
|
||||
|
||||
job_and_notifications = [
|
||||
jobs = [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
@@ -105,24 +73,52 @@ def service_dashboard(service_id):
|
||||
".view_job", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"created_at": job["created_at"],
|
||||
"processing_finished": job["processing_finished"],
|
||||
"processing_started": job["processing_started"],
|
||||
"processing_finished": job.get("processing_finished"),
|
||||
"processing_started": job.get("processing_started"),
|
||||
"notification_count": job["notification_count"],
|
||||
"created_by": job["created_by"],
|
||||
"notifications": aggregate_notifications_by_job.get(job["id"], []),
|
||||
"template_name": job["template_name"],
|
||||
"original_file_name": job["original_file_name"],
|
||||
}
|
||||
for job in job_response
|
||||
if aggregate_notifications_by_job.get(job["id"], [])
|
||||
if job["job_status"] != "cancelled"
|
||||
]
|
||||
return render_template(
|
||||
"views/dashboard/dashboard.html",
|
||||
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||
partials=get_dashboard_partials(service_id),
|
||||
job_and_notifications=job_and_notifications,
|
||||
jobs=jobs,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
sms_sent=sms_sent,
|
||||
sms_allowance_remaining=sms_allowance_remaining,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/daily_stats.json")
|
||||
def get_daily_stats():
|
||||
service_id = session.get("service_id")
|
||||
date_range = get_stats_date_range()
|
||||
|
||||
stats = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@main.route("/daily_stats_by_user.json")
|
||||
def get_daily_stats_by_user():
|
||||
service_id = session.get("service_id")
|
||||
date_range = get_stats_date_range()
|
||||
user_id = current_user.id
|
||||
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
user_id,
|
||||
start_date=date_range["start_date"],
|
||||
days=date_range["days"],
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard.json")
|
||||
@user_has_permissions("view_activity")
|
||||
def service_dashboard_updates(service_id):
|
||||
@@ -329,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"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -408,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
|
||||
|
||||
|
||||
@@ -471,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,6 +1,15 @@
|
||||
import os
|
||||
import secrets
|
||||
|
||||
from flask import abort, current_app, redirect, render_template, request, url_for
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
|
||||
from app import status_api_client
|
||||
@@ -23,8 +32,12 @@ def index():
|
||||
)
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
# handle unit tests
|
||||
|
||||
nonce = secrets.token_urlsafe()
|
||||
session["nonce"] = nonce
|
||||
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", token)
|
||||
url = url.replace("NONCE", nonce)
|
||||
url = url.replace("STATE", token)
|
||||
return render_template(
|
||||
"views/signedout.html",
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import csv
|
||||
import itertools
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
from flask import 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 (
|
||||
@@ -23,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,
|
||||
@@ -70,6 +82,40 @@ def platform_admin():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/download-all-users")
|
||||
@user_is_platform_admin
|
||||
def download_all_users():
|
||||
|
||||
# Create a CSV string from the user data
|
||||
users = user_api_client.get_all_users_detailed()
|
||||
|
||||
if len(users) == 0:
|
||||
return "No data to download."
|
||||
|
||||
output = StringIO()
|
||||
header = ["Name", "Email Address", "Phone Number", "Service"]
|
||||
fieldnames = ["name", "email_address", "mobile_number", "service"]
|
||||
writer = csv.DictWriter(
|
||||
output,
|
||||
fieldnames=fieldnames,
|
||||
delimiter=",",
|
||||
)
|
||||
# Write custom header
|
||||
writer.writerow(dict(zip(fieldnames, header)))
|
||||
for user in users:
|
||||
user_no_commas = {key: value.replace(",", "") for key, value in user.items()}
|
||||
if user_no_commas["name"].startswith("e2e"):
|
||||
continue
|
||||
writer.writerow(user_no_commas)
|
||||
csv_data = output.getvalue()
|
||||
|
||||
# Create a direct download response with the CSV data and appropriate headers
|
||||
response = Response(csv_data, content_type="text/csv; charset=utf-8")
|
||||
response.headers["Content-Disposition"] = "attachment; filename=users.csv"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def is_over_threshold(number, total, threshold):
|
||||
percentage = number / total * 100 if total else 0
|
||||
return percentage > threshold
|
||||
@@ -735,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
|
||||
|
||||
@@ -230,7 +230,12 @@ def set_up_your_profile():
|
||||
debug_msg(
|
||||
f"Added user {usr.email_address} to service {invite_data['service_id']}"
|
||||
)
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
# notify-admin-1766
|
||||
# redirect new users to templates area of new service instead of dashboard
|
||||
service_id = invite_data["service_id"]
|
||||
url = url_for(".service_dashboard", service_id=service_id)
|
||||
url = f"{url}/templates"
|
||||
return redirect(url)
|
||||
|
||||
elif login_gov_error:
|
||||
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
||||
|
||||
@@ -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,
|
||||
@@ -941,8 +943,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 +953,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 +1004,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-{service_id}-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 +1091,7 @@ def get_spreadsheet_column_headings_from_template(template):
|
||||
|
||||
|
||||
def get_recipient():
|
||||
set_timezone()
|
||||
if {"recipient", "placeholders"} - set(session.keys()):
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
|
||||
@@ -12,6 +14,7 @@ from flask import (
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
@@ -28,21 +31,19 @@ from app.utils.user import is_gov_user
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
|
||||
def _reformat_keystring(orig):
|
||||
new_keystring = orig.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
new_keystring = new_keystring.replace("-----END PRIVATE KEY-----", "")
|
||||
new_keystring = new_keystring.strip()
|
||||
new_keystring = new_keystring.replace(" ", "\n")
|
||||
new_keystring = "\n".join(
|
||||
["-----BEGIN PRIVATE KEY-----", new_keystring, "-----END PRIVATE KEY-----"]
|
||||
)
|
||||
new_keystring = f"{new_keystring}\n"
|
||||
def _reformat_keystring(orig): # pragma: no cover
|
||||
arr = orig.split("-----")
|
||||
begin = arr[1]
|
||||
end = arr[3]
|
||||
middle = arr[2].strip()
|
||||
new_keystring = f"-----{begin}-----\n{middle}\n-----{end}-----\n"
|
||||
return new_keystring
|
||||
|
||||
|
||||
def _get_access_token(code, state):
|
||||
def _get_access_token(code, state): # pragma: no cover
|
||||
client_id = os.getenv("LOGIN_DOT_GOV_CLIENT_ID")
|
||||
access_token_url = os.getenv("LOGIN_DOT_GOV_ACCESS_TOKEN_URL")
|
||||
certs_url = os.getenv("LOGIN_DOT_GOV_CERTS_URL")
|
||||
keystring = os.getenv("LOGIN_PEM")
|
||||
if " " in keystring:
|
||||
keystring = _reformat_keystring(keystring)
|
||||
@@ -63,15 +64,48 @@ 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:
|
||||
response_json = response.json()
|
||||
try:
|
||||
encoded_id_token = response_json["id_token"]
|
||||
except KeyError as e:
|
||||
# Capture the response json here so it hopefully shows up in error reports
|
||||
current_app.logger.error(f"Error when getting access token {response.json()}")
|
||||
raise KeyError(f"'access_token' {response.json()}")
|
||||
access_token = response.json()["access_token"]
|
||||
current_app.logger.exception(f"Error when getting id token {response_json}")
|
||||
raise KeyError(f"'access_token' {response.json()}") from e
|
||||
|
||||
# Getting Login.gov signing keys for unpacking the id_token correctly.
|
||||
jwks = requests.get(certs_url).json()
|
||||
public_keys = {
|
||||
jwk["kid"]: {
|
||||
"key": jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)),
|
||||
"algo": jwk["alg"],
|
||||
}
|
||||
for jwk in jwks["keys"]
|
||||
}
|
||||
kid = jwt.get_unverified_header(encoded_id_token)["kid"]
|
||||
pub_key = public_keys[kid]["key"]
|
||||
algo = public_keys[kid]["algo"]
|
||||
id_token = jwt.decode(
|
||||
encoded_id_token, pub_key, audience=client_id, algorithms=[algo]
|
||||
)
|
||||
|
||||
nonce = id_token["nonce"]
|
||||
saved_nonce = session.pop("nonce")
|
||||
if nonce != saved_nonce:
|
||||
current_app.logger.error(f"Nonce Error: {nonce} != {saved_nonce}")
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
access_token = response_json["access_token"]
|
||||
except KeyError as e:
|
||||
# Capture the response json here so it hopefully shows up in error reports
|
||||
current_app.logger.exception(
|
||||
f"Error when getting access token {response.json()} #notify-admin-1505"
|
||||
)
|
||||
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(
|
||||
@@ -83,14 +117,16 @@ 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:
|
||||
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
||||
current_app.logger.error(
|
||||
f"login.gov error: {login_gov_error} #notify-admin-1505"
|
||||
)
|
||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||
elif code and state:
|
||||
|
||||
@@ -100,12 +136,15 @@ def _do_login_dot_gov():
|
||||
user_email, user_uuid = _get_user_email_and_uuid(access_token)
|
||||
if not is_gov_user(user_email):
|
||||
current_app.logger.error(
|
||||
"invited user has a non-government email address."
|
||||
"invited user has a non-government email address. #notify-admin-1505"
|
||||
)
|
||||
flash("You must use a government email address.")
|
||||
abort(403)
|
||||
redirect_url = request.args.get("next")
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
current_app.logger.info(
|
||||
f"Retrieved user {user['id']} from db #notify-admin-1505"
|
||||
)
|
||||
|
||||
# Check if the email needs to be revalidated
|
||||
is_fresh_email = is_less_than_days_ago(
|
||||
@@ -115,16 +154,17 @@ def _do_login_dot_gov():
|
||||
return verify_email(user, redirect_url)
|
||||
|
||||
usr = User.from_email_address(user["email_address"])
|
||||
current_app.logger.info(f"activating user {usr.id} #notify-admin-1505")
|
||||
activate_user(usr.id)
|
||||
except BaseException as be: # noqa B036
|
||||
current_app.logger.error(be)
|
||||
current_app.logger.exception(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")
|
||||
@@ -133,19 +173,35 @@ 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"])
|
||||
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 see the admin logs
|
||||
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(None)
|
||||
|
||||
# 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
|
||||
@@ -157,11 +213,6 @@ 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)
|
||||
@@ -173,10 +224,15 @@ def sign_in():
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
|
||||
nonce = secrets.token_urlsafe()
|
||||
session["nonce"] = nonce
|
||||
|
||||
# handle unit tests
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", token)
|
||||
url = url.replace("NONCE", nonce)
|
||||
url = url.replace("STATE", token)
|
||||
|
||||
return render_template(
|
||||
"views/signin.html",
|
||||
again=bool(redirect_url),
|
||||
@@ -185,5 +241,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))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -38,6 +38,7 @@ def verify_email(token):
|
||||
current_app.config["EMAIL_EXPIRY_SECONDS"],
|
||||
)
|
||||
except SignatureExpired:
|
||||
current_app.logger.error("Email link expired #notify-admin-1505")
|
||||
flash(
|
||||
"The link in the email we sent you has expired. We've sent you a new one."
|
||||
)
|
||||
@@ -50,6 +51,9 @@ def verify_email(token):
|
||||
abort(404)
|
||||
|
||||
if user.is_active:
|
||||
current_app.logger.error(
|
||||
f"User is using an invite link but is already logged in {user.id} #notify-admin-1505"
|
||||
)
|
||||
flash("That verification link has expired.")
|
||||
return redirect(url_for("main.sign_in"))
|
||||
|
||||
@@ -59,6 +63,7 @@ def verify_email(token):
|
||||
|
||||
user.send_verify_code()
|
||||
session["user_details"] = {"email": user.email_address, "id": user.id}
|
||||
current_app.logger.info(f"Email verified for user {user.id} #notify-admin-1505")
|
||||
return redirect(url_for("main.verify"))
|
||||
|
||||
|
||||
@@ -78,5 +83,7 @@ def activate_user(user_id):
|
||||
return redirect(url_for("main.organization_dashboard", org_id=organization_id))
|
||||
else:
|
||||
activated_user = user.activate()
|
||||
current_app.logger.info(f"Activated user {user.id} #notify-admin-1505")
|
||||
activated_user.login()
|
||||
current_app.logger.info(f"Logged in user {user.id} #notify-admin-1505")
|
||||
return redirect(url_for("main.add_service", first="first"))
|
||||
|
||||
@@ -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):
|
||||
@@ -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(
|
||||
|
||||
@@ -123,6 +123,7 @@ class HeaderNavigation(Navigation):
|
||||
"get_billing_report",
|
||||
"get_users_report",
|
||||
"get_daily_volumes",
|
||||
"download_all_users",
|
||||
"get_daily_sms_provider_volumes",
|
||||
"get_volumes_by_service",
|
||||
"organizations",
|
||||
@@ -153,6 +154,9 @@ class HeaderNavigation(Navigation):
|
||||
|
||||
class MainNavigation(Navigation):
|
||||
mapping = {
|
||||
"activity": {
|
||||
"all_jobs_activity",
|
||||
},
|
||||
"dashboard": {
|
||||
"conversation",
|
||||
"inbox",
|
||||
|
||||
@@ -56,6 +56,9 @@ class NotifyAdminAPIClient(BaseAPIClient):
|
||||
):
|
||||
abort(403)
|
||||
|
||||
def is_calling_signin_url(self, arg):
|
||||
return arg.startswith("('/user")
|
||||
|
||||
def check_inactive_user(self, *args):
|
||||
still_signing_in = False
|
||||
|
||||
@@ -64,14 +67,7 @@ class NotifyAdminAPIClient(BaseAPIClient):
|
||||
# and we only want to check the first arg
|
||||
for arg in args:
|
||||
arg = str(arg)
|
||||
if (
|
||||
"get-login-gov-user" in arg
|
||||
or "user/email" in arg
|
||||
or "/activate" in arg
|
||||
or "/email-code" in arg
|
||||
or "/verify/code" in arg
|
||||
or "/user" in arg
|
||||
):
|
||||
if self.is_calling_signin_url(arg):
|
||||
still_signing_in = True
|
||||
|
||||
# This seems to be a weird edge case that happens intermittently with invites
|
||||
|
||||
@@ -27,9 +27,7 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
|
||||
def get_job(self, service_id, job_id):
|
||||
params = {}
|
||||
job = self.get(
|
||||
url="/service/{}/job/{}".format(service_id, job_id), params=params
|
||||
)
|
||||
job = self.get(url=f"/service/{service_id}/job/{job_id}", params=params)
|
||||
|
||||
return job
|
||||
|
||||
@@ -40,13 +38,14 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
if statuses is not None:
|
||||
params["statuses"] = ",".join(statuses)
|
||||
|
||||
return self.get(url="/service/{}/job".format(service_id), params=params)
|
||||
job = self.get(url=f"/service/{service_id}/job", params=params)
|
||||
return job
|
||||
|
||||
def get_uploads(self, service_id, limit_days=None, page=1):
|
||||
params = {"page": page}
|
||||
if limit_days is not None:
|
||||
params["limit_days"] = limit_days
|
||||
return self.get(url="/service/{}/upload".format(service_id), params=params)
|
||||
return self.get(url=f"/service/{service_id}/upload", params=params)
|
||||
|
||||
def has_sent_previously(
|
||||
self, service_id, template_id, template_version, original_file_name
|
||||
|
||||
@@ -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):
|
||||
@@ -157,6 +159,10 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
endpoint = "/user"
|
||||
return self.get(endpoint)["data"]
|
||||
|
||||
def get_all_users_detailed(self):
|
||||
endpoint = "/user/report-all-users"
|
||||
return self.get(endpoint)["data"]
|
||||
|
||||
@cache.delete("service-{service_id}")
|
||||
@cache.delete("service-{service_id}-template-folders")
|
||||
@cache.delete("user-{user_id}")
|
||||
|
||||
@@ -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,7 +47,7 @@
|
||||
{% 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">
|
||||
@@ -78,7 +78,7 @@
|
||||
{% 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 %}
|
||||
|
||||
@@ -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>
|
||||
@@ -11,17 +11,16 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
|
||||
<link rel="shortcut icon" href="{{ assetPath | default('/assets') }}/images/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<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="{{ asset_url('images/site.webmanifest') }}">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<link href="{{ assetPath | default('/assets') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<link href="{{ assetPath | default('/static') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
|
||||
<link rel="stylesheet" media="screen" href="{{ asset_url('css/styles.css') }}" />
|
||||
{% block extra_stylesheets %}{% endblock %}
|
||||
|
||||
@@ -31,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,26 +1,18 @@
|
||||
{% 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">
|
||||
{% 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>
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('activity') }}" href="{{ url_for('.all_jobs_activity', service_id=current_service.id) }}">Activity</a></li>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
@@ -6,7 +6,7 @@
|
||||
<thead class="table-field-headings{% if field_headings_visible %}-visible{% endif %}">
|
||||
<tr>
|
||||
{% for field_heading in field_headings %}
|
||||
<th scope="col" class="table-field-heading{% if loop.first %}-first{% endif %}" width="{% if equal_length %}{{ (100 / field_headings|length)|int }}%{% endif %}">
|
||||
<th class="table-field-heading{% if loop.first %}-first{% endif %}" width="{% if equal_length %}{{ (100 / field_headings|length)|int }}%{% endif %}">
|
||||
{% if field_headings_visible %}
|
||||
{{ field_heading }}
|
||||
{% else %}
|
||||
@@ -79,9 +79,9 @@
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro row_heading() -%}
|
||||
<th class="table-field">
|
||||
<td>
|
||||
{{ caller() }}
|
||||
</th>
|
||||
</td>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro index_field(text=None, rowspan=None) -%}
|
||||
|
||||
@@ -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('/assets') }}/images/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32"href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png"/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<link href="{{ assetPath | default('/assets') }}/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 %}
|
||||
@@ -22,7 +22,7 @@
|
||||
{% else %}
|
||||
|
||||
{% if notifications %}
|
||||
<div class="dashboard-table bottom-gutter-3-2">
|
||||
<div class="dashboard-table job-status-table table-overflow-x-auto">
|
||||
{% endif %}
|
||||
{% if job.still_processing %}
|
||||
<p class="bottom-gutter hint">
|
||||
@@ -40,15 +40,16 @@
|
||||
notifications,
|
||||
caption=uploaded_file_name,
|
||||
caption_visible=False,
|
||||
border_visible=True,
|
||||
empty_message='No messages to show yet…' if job.awaiting_processing_or_recently_processed else 'These messages have been deleted because they were sent more than {} days ago'.format(service_data_retention_days),
|
||||
field_headings=[
|
||||
'Recipient',
|
||||
'Status'
|
||||
'Message status'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call row_heading() %}
|
||||
<a class="usa-link file-list-filename" href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=item.id, from_job=job.id) }}">{{ item.to }}</a>
|
||||
<a class="usa-link file-list-filename" href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=item.id, from_job=job.id) }}">{{ item.to | format_phone_number_human_readable }}</a>
|
||||
<p class="file-list-hint">
|
||||
{{ item.preview_of_content }}
|
||||
</p>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, notification_status_field %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
<div class="ajax-block-container table-wrapper" aria-labelledby='pill-selected-item'>
|
||||
<div class="dashboard-table bottom-gutter-3-2">
|
||||
<div class="ajax-block-container" aria-labelledby='pill-selected-item'>
|
||||
<div class="dashboard-table table-overflow-x-auto">
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
[notification],
|
||||
|
||||
143
app/templates/views/activity/all-activity.html
Normal file
143
app/templates/views/activity/all-activity.html
Normal file
@@ -0,0 +1,143 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
|
||||
{% block service_page_title %}
|
||||
All activity
|
||||
{% endblock %}
|
||||
|
||||
{% set show_pagination %}
|
||||
{% if prev_page or next_page %}
|
||||
<nav aria-label="Pagination" class="usa-pagination">
|
||||
<ul class="usa-pagination__list">
|
||||
{% if prev_page %}
|
||||
<li class="usa-pagination__item usa-pagination__arrow">
|
||||
<a
|
||||
href="{{prev_page['url']}}"
|
||||
class="usa-pagination__link usa-pagination__previous-page"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<img src="{{ asset_url('img/usa-icons/navigate_before.svg') }}" alt="arrow">
|
||||
<span class="usa-pagination__link-text">Previous</span></a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if pagination %}
|
||||
{% for page in pagination.pages %}
|
||||
{% if page == pagination.current %}
|
||||
<li class="usa-pagination__item usa-pagination__page-no">
|
||||
<span class="usa-pagination__button usa-current" aria-label="Page {{ page }}" aria-current="true">
|
||||
{{ page }}
|
||||
</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="usa-pagination__item">
|
||||
<a class="usa-pagination__button" href="?page={{ page }}">
|
||||
{{ page }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if next_page %}
|
||||
<li class="usa-pagination__item usa-pagination__arrow">
|
||||
<a
|
||||
href="{{ next_page['url'] }}"
|
||||
class="usa-pagination__link usa-pagination__next-page"
|
||||
aria-label="Next page"
|
||||
>
|
||||
<span class="usa-pagination__link-text">Next </span>
|
||||
<img src="{{ asset_url('img/usa-icons/navigate_next.svg') }}" alt="arrow">
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endset %}
|
||||
{% block maincolumn_content %}
|
||||
<div class="margin-bottom-8">
|
||||
<h1 class="usa-sr-only">All activity</h1>
|
||||
<h2 class="font-body-2xl line-height-sans-2 margin-0">All activity</h2>
|
||||
<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>
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId">
|
||||
<span>Job ID#</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Template</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Time sent</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Sender</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Report</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Delivered</span>
|
||||
</th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading">
|
||||
<span>Failed</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if all_jobs_dict %}
|
||||
{% for job in all_jobs_dict %}
|
||||
<tr class="table-row">
|
||||
<td class="table-field jobid" scope="row" role="rowheader">
|
||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-field template">{{ job.template_name }}</td>
|
||||
<td data-sort-value="{{job.time_sent_data_value}}" class="table-field time-sent">
|
||||
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||
</td>
|
||||
<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>
|
||||
{% else %}
|
||||
<span>N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="table-field delivered">{{ job.delivered_count if job.delivered_count is not none else '0' }}</td>
|
||||
<td class="table-field failed">{{ job.failed_count if job.failed_count is not none else '0' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr class="table-row">
|
||||
<td class="table-empty-message" colspan="10">No messages found</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="usa-sr-only usa-table__announcement-region" aria-live="polite"></div>
|
||||
<p><b>Note: </b>Report data is only available for 7 days after your message has been sent</p>
|
||||
</div>
|
||||
{{show_pagination}}
|
||||
{% if current_user.has_permissions('view_activity') %}
|
||||
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Download recent reports</h2>
|
||||
<p class="font-body-sm">
|
||||
<a href="{{ download_link_one_day }}" download="download" class="usa-link">Download all data last 24 hours (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
</p>
|
||||
<p class="font-body-sm">
|
||||
<a href="{{ download_link_three_day }}" download="download" class="usa-link">Download all data last 3 days (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
 
|
||||
</p>
|
||||
<p class="font-body-sm">
|
||||
<a href="{{ download_link_five_day }}" download="download" class="usa-link">Download all data last 5 days (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
</p>
|
||||
<p class="font-body-sm">
|
||||
<a href="{{ download_link_seven_day }}" download="download" class="usa-link">Download all data last 7 days (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="ajax-block-container" id='pill-selected-item'>
|
||||
|
||||
{% if notifications %}
|
||||
<div class="table-wrapper">
|
||||
<div class="table-overflow-x-auto">
|
||||
<div class='dashboard-table'>
|
||||
{% endif %}
|
||||
{% call(item, row_number) list_table(
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
{{ page_header('Callbacks') }}
|
||||
<div class="table-wrapper">
|
||||
<div class="bottom-gutter-3-2 dashboard-table body-copy-table">
|
||||
<div class="table-overflow-x-auto">
|
||||
<div class="bottom-gutter-3-2 dashboard-table body-copy-table table-overflow-x-auto">
|
||||
{% call mapping_table(
|
||||
caption='General',
|
||||
field_headings=['Label', 'Value', 'Action'],
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="usa-table-container--scrollable" tabindex="0">
|
||||
<div class="usa-table-container--scrollable table-overflow-x-auto" tabindex="0">
|
||||
<table class="usa-table usa-table--borderless width-full">
|
||||
<caption class="font-body-lg table-heading">
|
||||
Note: Only the first 5 rows are displayed here.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
||||
|
||||
<div class="table-wrapper">
|
||||
<div class="table-overflow-x-auto">
|
||||
<div class='dashboard-table ajax-block-container'>
|
||||
{% call(item, row_number) list_table(
|
||||
jobs,
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
No messages sent
|
||||
</p>
|
||||
{% else %}
|
||||
<div class='template-statistics-table'>
|
||||
<div class='template-statistics-table table-overflow-x-auto'>
|
||||
{% call(item, row_number) list_table(
|
||||
month.templates_used,
|
||||
caption=month.name,
|
||||
|
||||
@@ -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,8 +10,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
||||
|
||||
<div class="dashboard margin-bottom-8">
|
||||
|
||||
<div class="dashboard margin-bottom-2">
|
||||
|
||||
<h1 class="usa-sr-only">Dashboard</h1>
|
||||
{% if current_user.has_permissions('manage_templates') and not current_service.all_templates %}
|
||||
@@ -18,97 +22,122 @@
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'upcoming') }}
|
||||
|
||||
<h2 class="font-body-2xl line-height-sans-2 margin-0">
|
||||
Messages sent
|
||||
</h2>
|
||||
<h2 class="font-body-xl margin-top-0">{{ current_service.name }} Dashboard</h2>
|
||||
|
||||
<!-- <button id="sevenDaysButton">7 Days</button>
|
||||
<canvas id="myChart"></canvas> -->
|
||||
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'totals') }}
|
||||
<div id="totalMessageChartContainer" data-sms-sent="{{ sms_sent }}" data-sms-allowance-remaining="{{ sms_allowance_remaining }}">
|
||||
<h2 id="chartTitle">Total messages</h2>
|
||||
<svg id="totalMessageChart"></svg>
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
<div id="totalMessageTable" class="margin-0"></div>
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||
<h2 class="margin-top-4 margin-bottom-1">Recent Batches</h2>
|
||||
<div class="table-wrapper">
|
||||
<table class="usa-table usa-table--borderless job-table">
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" class="table-field-heading-first">
|
||||
<span>File name</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Template</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Job status</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Sender</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span># of Recipients</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Report</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if job_and_notifications %}
|
||||
{% for job in job_and_notifications[:5] %}
|
||||
{% if job.job_id and job.notifications %}
|
||||
<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="service" selected>{{ current_service.name }}</option>
|
||||
<option value="individual">{{ current_user.name }}</option>
|
||||
</select>
|
||||
</form>
|
||||
<div id="activityChart">
|
||||
<div class="chart-header">
|
||||
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</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>
|
||||
</div>
|
||||
</div>
|
||||
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
||||
|
||||
<div class="table-container">
|
||||
<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">
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||
<th data-sortable scope="col" class="table-field-heading"><span>Template</span></th>
|
||||
<th data-sortable scope="col" class="table-field-heading"><span>Job status</span></th>
|
||||
<th data-sortable scope="col" class="table-field-heading"><span># of Recipients</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if jobs %}
|
||||
{% for job in jobs[:5] %}
|
||||
{% 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">
|
||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-field template">{{ job.template_name }}</td>
|
||||
<td class="table-field time-sent">Sent on
|
||||
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||
</td>
|
||||
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Template</span></th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Job status</span></th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Sender</span></th>
|
||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span># of Recipients</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if jobs %}
|
||||
{% for job in jobs[:5] %}
|
||||
{% set notification = job.notifications[0] %}
|
||||
<tr class="table-row" id="{{ job.job_id }}">
|
||||
<td class="table-field file-name">
|
||||
{{ notification.job.original_file_name[:12] if notification.job.original_file_name else 'Manually entered number'}}
|
||||
<br>
|
||||
<a class="usa-link file-list-filename" href="{{ job.view_job_link }}">View Batch</a>
|
||||
<tr id="{{ job.job_id }}">
|
||||
<td class="table-field jobid" scope="row" role="rowheader">
|
||||
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-field template">
|
||||
{{ notification.template.name }}
|
||||
</td>
|
||||
<td class="table-field time-sent">
|
||||
<td class="table-field template">{{ job.template_name }}</td>
|
||||
<td class="table-field time-sent">Sent on
|
||||
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||
</td>
|
||||
<td class="table-field sender">
|
||||
{{ notification.created_by.name }}
|
||||
</td>
|
||||
<td class="table-field count-of-recipients">
|
||||
{{ job.notification_count}}
|
||||
</td>
|
||||
<td class="table-field report">
|
||||
{% if notification and job.time_left != "Data no longer available" %}
|
||||
<a class="usa-link file-list-filename" href="{{ job.download_link }}">Download</a>
|
||||
<span class="usa-hint">{{ job.time_left }}</span>
|
||||
{% elif job %}
|
||||
<span>{{ job.time_left }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="table-field sender">{{ job.created_by.name }}</td>
|
||||
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% 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>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Message count</h2>
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
<h3 class='margin-bottom-0' id="current-year"></h3>
|
||||
{{ ajax_block(partials, updates_url, 'usage') }}
|
||||
<p class="margin-top-0">During the pilot period, each service has an allowance of 250,000 message parts. Once this allowance is met, the
|
||||
application will stop delivering messages. There's no monthly charge, no setup fee, and no procurement cost.</p>
|
||||
<p class="align-with-heading-copy">
|
||||
What counts as 1 text message part?<br />
|
||||
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,33 +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 > 1 %}
|
||||
<div class='template-statistics-table'>
|
||||
{% call(item, row_number) list_table(
|
||||
template_statistics,
|
||||
caption="Messages sent by template",
|
||||
caption_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>
|
||||
<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>
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<p class="usa-body">To send a text with Notify, you need to create a reusable message template first.</p>
|
||||
<p class="usa-body">Templates let you send the same thing to lots of people, as often as you need to, without writing a new message each time.</p>
|
||||
|
||||
<h2 class="heading-medium" id="organizing-content">Organizing your content</h2>
|
||||
<p class="usa-body">You can organize your content by grouping templates, and putting them into folders.</p>
|
||||
<p class="usa-body">Set up folders within the Send message section of Notify, where you find templates. You can choose to create a Folder before or after a template is made. If you want to change a user’s ability to view or edit folders, you may control this through their individual team member permissions.</p>
|
||||
|
||||
<h2 class="heading-medium" id="personalised-messages">Personalized content</h2>
|
||||
<p class="usa-body">Notify makes it easy to send personalized messages from a single template.</p>
|
||||
<p class="usa-body">See <a class="usa-link" href="{{ url_for('.guidance_index', _anchor='personalized-content') }}">how to personalize your content</a>.</p>
|
||||
@@ -44,7 +48,6 @@
|
||||
<p class="usa-body">Notify lets you:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>set different permission levels for each team member</li>
|
||||
<li>invite team members who do not have a government email address</li>
|
||||
<li>choose who else can manage team members</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -162,55 +162,57 @@
|
||||
location, the flag to receive the content needs to be captured in its own column with a Yes or No (Y/N) flag.</p>
|
||||
|
||||
<h3>Example</h3>
|
||||
<table class="usa-table">
|
||||
<caption class="usa-sr-only">
|
||||
Example
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Phone number</th>
|
||||
<th scope="col">First name</th>
|
||||
<th scope="col">Last name</th>
|
||||
<th scope="col">Spanish</th>
|
||||
<th scope="col">English</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Lulu</td>
|
||||
<td>Praether</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>3:25</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>234-567-8901</td>
|
||||
<td>Sela</td>
|
||||
<td>Appel</td>
|
||||
<td>Y</td>
|
||||
<td>N</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>4:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Dexter</td>
|
||||
<td>Moseley</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>2:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-overflow-x-auto">
|
||||
<table class="usa-table">
|
||||
<caption class="usa-sr-only">
|
||||
Example
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Phone number</th>
|
||||
<th scope="col">First name</th>
|
||||
<th scope="col">Last name</th>
|
||||
<th scope="col">Spanish</th>
|
||||
<th scope="col">English</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Lulu</td>
|
||||
<td>Praether</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>3:25</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>234-567-8901</td>
|
||||
<td>Sela</td>
|
||||
<td>Appel</td>
|
||||
<td>Y</td>
|
||||
<td>N</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>4:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Dexter</td>
|
||||
<td>Moseley</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>2:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# Preventing fraud #}
|
||||
<h2 class="padding-top-1" id="prevent-fraud">Preventing fraud</h2>
|
||||
|
||||
@@ -23,36 +23,34 @@ Inbound SMS
|
||||
<h1 class="font-body-2xl">
|
||||
Inbound SMS
|
||||
</h1>
|
||||
|
||||
<table class="usa-table usa-table--borderless inbound">
|
||||
<thread>
|
||||
<tr>
|
||||
<th>{{table_headings.field_headings[0]}}</th>
|
||||
<th>{{table_headings.field_headings[1]}}</th>
|
||||
<th>{{table_headings.field_headings[2]}}</th>
|
||||
</tr>
|
||||
</thread>
|
||||
<tbody>
|
||||
|
||||
{% for value in inbound_num_list.data: %}
|
||||
<div class="table-overflow-x-auto">
|
||||
<table class="usa-table usa-table--borderless inbound width-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{value.number}}</td>
|
||||
<td>
|
||||
|
||||
{% if value.active %}
|
||||
Active
|
||||
{% elif not value.service.name %}
|
||||
Not used
|
||||
{% else %}
|
||||
Inactive
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('main.service_dashboard', service_id=value.service.id) }}" class="usa-link bold">{{ value.service.name }}</a>
|
||||
</td>
|
||||
<th>{{table_headings.field_headings[0]}}</th>
|
||||
<th>{{table_headings.field_headings[1]}}</th>
|
||||
<th>{{table_headings.field_headings[2]}}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for value in inbound_num_list.data: %}
|
||||
<tr>
|
||||
<td>{{value.number}}</td>
|
||||
<td>
|
||||
{% if value.active %}
|
||||
Active
|
||||
{% elif not value.service.name %}
|
||||
Not used
|
||||
{% else %}
|
||||
Inactive
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('main.service_dashboard', service_id=value.service.id) }}" class="usa-link bold">{{ value.service.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Invite {}'.format(user_to_invite.name if user_to_invite else 'a team member')) }}
|
||||
|
||||
<a href="#permissions">Learn more about user permissions.</a>
|
||||
{% call form_wrapper() %}
|
||||
|
||||
{% if user_to_invite %}
|
||||
@@ -36,5 +36,99 @@
|
||||
{{ page_footer('Send invitation email') }}
|
||||
|
||||
{% endcall %}
|
||||
<h2 id="permissions">Set up user permissions</h2>
|
||||
<p>Setting the right digital control is one of many aspects to keep content and information on a need to know basis. Permissions allow you to do this. Give team members access to important views related to their responsibilities.</p>
|
||||
<h3 class="site-preview-heading">Permissions</h3>
|
||||
|
||||
<div class="usa-accordion usa-accordion--bordered margin-top-1">
|
||||
<h4 class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button"
|
||||
aria-expanded="true"
|
||||
aria-controls="b-a1"
|
||||
>
|
||||
See dashboard
|
||||
</button>
|
||||
</h4>
|
||||
<div id="b-a1" class="usa-accordion__content usa-prose">
|
||||
<p>Allows users to view the dashboard but not have access to any other functions on the site.
|
||||
Dashboard viewers can see: Weekly message quantity</p>
|
||||
<ul>
|
||||
<li>Message templates used</li>
|
||||
<li>Recent batches of sent messages</li>
|
||||
</ul>
|
||||
<p>This also gives access to:</p>
|
||||
<ul>
|
||||
<li>List of templates used by month</li>
|
||||
<li>Batch details with message status</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h4 class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button"
|
||||
aria-expanded="false"
|
||||
aria-controls="b-a2"
|
||||
>
|
||||
Send messages
|
||||
</button>
|
||||
</h4>
|
||||
<div id="b-a2" class="usa-accordion__content usa-prose">
|
||||
<p>
|
||||
Allows users to view, select and send approved messages, but restricts users from editing templates
|
||||
and/or folders. Team members responsible for sending message batches can use templates to prepare
|
||||
and schedule texts. Actions they take in this section:
|
||||
</p>
|
||||
<ul>
|
||||
<li>View and use templates</li>
|
||||
<li>Enter personalized message field content</li>
|
||||
<li>Schedule messages for sending</li>
|
||||
<li>Send messages</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h4 class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button"
|
||||
aria-expanded="false"
|
||||
aria-controls="b-a3"
|
||||
>
|
||||
Add and edit templates
|
||||
</button>
|
||||
</h4>
|
||||
<div id="b-a3" class="usa-accordion__content usa-prose">
|
||||
<p>
|
||||
Allows users to view, create or edit, and more or delete templates and folders. They will set up real
|
||||
messages, but are restricted from uploading data or sending or seeing previous versions. Some actions
|
||||
they can take:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Create a new template</li>
|
||||
<li>Create a new folder</li>
|
||||
<li>Move (or organize) folders and templates</li>
|
||||
<li>Delete templates and/or folders</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h4 class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button"
|
||||
aria-expanded="false"
|
||||
aria-controls="b-a4"
|
||||
>
|
||||
Manage settings and team
|
||||
</button>
|
||||
</h4>
|
||||
<div id="b-a4" class="usa-accordion__content usa-prose">
|
||||
<p>Leads with service management responsibility may do so in Settings. Managed within Settings:</p>
|
||||
<ul>
|
||||
<li>Change service name</li>
|
||||
<li>Invite team members</li>
|
||||
<li>Remove unnecessary team members</li>
|
||||
<li>Turn off the ability to send text messages</li>
|
||||
<li>Turn off the ability to send service name with text message</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="notification-status">
|
||||
Messages will remain in pending state until carrier status is received, typically 5 minutes.
|
||||
<p class="notification-status max-width-full">
|
||||
Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear back from the carrier they have received it and attempted deliver. More information on <a class="usa-link" href="{{ url_for('main.message_status') }}">delivery status</a>.
|
||||
</p>
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% block serviceNavigation %}{% endblock %}
|
||||
|
||||
{% block sideNavigation %}
|
||||
{% include "/new/components/settings_nav.html" %}
|
||||
{% include "components/settings_nav.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<p>Notify cannot tell you if your users open an email or click on the links in an email. We do not track open rates and click-throughs because there are privacy issues. Tracking emails without asking permission from users could breach General Data Protection Regulations (GDPR).</p> -->
|
||||
|
||||
<h2 id="text-message-statuses" class="heading-medium">Text messages</h2>
|
||||
<div class="margin-bottom-8">
|
||||
<div class="margin-bottom-8 table-overflow-x-auto">
|
||||
{% call mapping_table(
|
||||
caption='Message statuses – text messages',
|
||||
field_headings=['Status', 'Description'],
|
||||
|
||||
@@ -44,33 +44,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
notifications_by_type|reverse,
|
||||
caption='Messages sent since May 2023',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Date',
|
||||
99|message_count_noun('email')|capitalize,
|
||||
99|message_count_noun('sms')|capitalize,
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.date | format_date_normal }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.emails|format_thousands }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.sms|format_thousands }}
|
||||
{% endcall %}
|
||||
<div class="dashboard-table table-overflow-x-auto">
|
||||
{% call(item, row_number) list_table(
|
||||
notifications_by_type|reverse,
|
||||
caption='Messages sent since May 2023',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Date',
|
||||
99|message_count_noun('email')|capitalize,
|
||||
99|message_count_noun('sms')|capitalize,
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.date | format_date_normal }}
|
||||
{% endcall %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the last {{ notifications_by_type|length }} days
|
||||
</p>
|
||||
</div>
|
||||
{% call field() %}
|
||||
{{ item.emails|format_thousands }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.sms|format_thousands }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the last {{ notifications_by_type|length }} days
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="govuk-heading-m">
|
||||
@@ -84,7 +82,7 @@
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<div class="table-overflow-x-auto">
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
processing_time | reverse,
|
||||
@@ -124,7 +122,7 @@
|
||||
<span class="usa-sr-only">using Notify.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<div class="table-overflow-x-auto">
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
organizations_using_notify,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="grid-row margin-bottom-10">
|
||||
<div class="tablet:grid-col-3 margin-top-4">
|
||||
<div class="tablet:grid-col-2 margin-top-4">
|
||||
<nav class="navigation">
|
||||
<ul class="usa-sidenav">
|
||||
|
||||
@@ -41,11 +41,11 @@
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="grid-col-9 tablet:grid-col-9 padding-left-4 sub-content">
|
||||
<div class="grid-col-10 tablet:grid-col-10 padding-left-4 sub-content">
|
||||
{% 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>
|
||||
|
||||
@@ -13,26 +13,26 @@
|
||||
Email complaints
|
||||
</h1>
|
||||
|
||||
<div class="table-overflow-x-auto">
|
||||
{% call(item, row_number) list_table(
|
||||
complaints,
|
||||
caption="Complaints",
|
||||
caption_visible=False,
|
||||
empty_message='No complaints',
|
||||
field_headings=['Notification Id', 'Service', 'Complaint type', 'Complaint Date'],
|
||||
field_headings_visible=True
|
||||
) %}
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
complaints,
|
||||
caption="Complaints",
|
||||
caption_visible=False,
|
||||
empty_message='No complaints',
|
||||
field_headings=['Notification Id', 'Service', 'Complaint type', 'Complaint Date'],
|
||||
field_headings_visible=True
|
||||
) %}
|
||||
{{ link_field(item.notification_id, url_for('main.view_notification', service_id=item.service_id, notification_id=item.notification_id)) }}
|
||||
|
||||
{{ link_field(item.notification_id, url_for('main.view_notification', service_id=item.service_id, notification_id=item.notification_id)) }}
|
||||
{{ link_field(item.service_name, url_for('main.service_dashboard', service_id=item.service_id)) }}
|
||||
|
||||
{{ link_field(item.service_name, url_for('main.service_dashboard', service_id=item.service_id)) }}
|
||||
{{ text_field(item.complaint_type) }}
|
||||
|
||||
{{ text_field(item.complaint_type) }}
|
||||
|
||||
{{ text_field(item.complaint_date|format_datetime_table if item.complaint_date else None) }}
|
||||
|
||||
{% endcall %}
|
||||
{{ text_field(item.complaint_date|format_datetime_table if item.complaint_date else None) }}
|
||||
|
||||
{% endcall %}
|
||||
</div>
|
||||
{{ previous_next_navigation(prev_page, next_page) }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -31,4 +31,8 @@
|
||||
<p>
|
||||
<a class="usa-link" href="{{ url_for('main.get_users_report') }}">Users Report</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="usa-link" href="{{ url_for('main.download_all_users') }}">Download All Users</a>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
{% from "components/components/details/macro.njk" import usaDetails %}
|
||||
|
||||
{% macro services_table(services, caption) %}
|
||||
<div class="table-overflow-x-auto">
|
||||
{% call(item, row_number) mapping_table(
|
||||
caption=caption,
|
||||
caption_visible=False,
|
||||
@@ -27,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 %}
|
||||
@@ -48,6 +53,7 @@
|
||||
{% endfor %}
|
||||
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ part.</p>
|
||||
|
||||
<h4>Calculation of message parts without special characters</h4>
|
||||
|
||||
<div class="bottom-gutter-3-2">
|
||||
<div class="table-overflow-x-auto">
|
||||
{% call mapping_table(
|
||||
caption='Text message pricing',
|
||||
field_headings=['Message length', 'Charge'],
|
||||
@@ -86,7 +86,7 @@ recipient’s preferred language, but we are aware that, rarely, a phone carrier
|
||||
to be calculated as detailed below.<p>
|
||||
<h4>Calculation of message parts with special characters or non-romanized scripts</h4>
|
||||
{% set accentedChars %}
|
||||
<div class="bottom-gutter-3-2">
|
||||
<div class="table-overflow-x-auto">
|
||||
{% call mapping_table(
|
||||
caption='Accented characters that affect text message charges',
|
||||
field_headings=['Character', 'Description'],
|
||||
@@ -151,7 +151,7 @@ to be calculated as detailed below.<p>
|
||||
"html": accentedChars
|
||||
}) }}
|
||||
|
||||
<div class="bottom-gutter-3-2">
|
||||
<div class="table-overflow-x-auto">
|
||||
{% call mapping_table(
|
||||
caption='Pricing for text messages containing accented characters that affect the charges',
|
||||
field_headings=['Message length', 'Charge'],
|
||||
|
||||
@@ -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) }}
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
{% block serviceNavigation %}{% endblock %}
|
||||
|
||||
{% block sideNavigation %}
|
||||
{% include "/new/components/settings_nav.html" %}
|
||||
{% include "components/settings_nav.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<h1 class="font-body-lg">Settings</h1>
|
||||
|
||||
<div class="bottom-gutter-3-2 settings-table body-copy-table">
|
||||
<div class="bottom-gutter-3-2 settings-table body-copy-table table-overflow-x-auto">
|
||||
|
||||
{% call mapping_table(
|
||||
caption='General',
|
||||
|
||||
@@ -20,7 +20,7 @@ Notify.gov
|
||||
<div class="desktop:grid-col-7 tablet:grid-col-12">
|
||||
<h1 class="font-serif-2xl usa-hero__heading">Reach people where they are with government-powered text messages</h1>
|
||||
<p class="font-sans-lg">Notify.gov is a text message service that helps federal, state, local, tribal and territorial governments more effectively communicate with the people they serve.</p>
|
||||
<div class="usa-button-group margin-bottom-5">
|
||||
<div class="usa-button-group margin-bottom-5 flex-align-center">
|
||||
<a class="usa-button usa-button login-button login-button--primary margin-right-2" href="{{ initial_signin_url }}">Sign in with <img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
|
||||
</a>
|
||||
if you are an existing pilot partner
|
||||
@@ -28,8 +28,8 @@ Notify.gov
|
||||
<p class="font-sans-md">Currently we are only working with select pilot partners. If you are interested in using Notify.gov in the future, please contact <br><a href="mailto:tts-benefits-studio@gsa.gov">tts-benefits-studio@gsa.gov</a> to learn more.</p>
|
||||
</div>
|
||||
<div class="desktop:grid-col-4 grid-offset-1 desktop:display-block display-none margin-x-5">
|
||||
<img src="{{ asset_url('images/product/phone-text.svg') }}"
|
||||
alt="An illustration of notifications to a phone.">
|
||||
<img src="{{ asset_url('images/product/phone-text.png') }}"
|
||||
alt="An illustration of notifications to a phone.">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
<nav id="template-list">
|
||||
<nav id="template-list" aria-label="Template list">
|
||||
{% set checkboxes_data = [] %}
|
||||
|
||||
{% if not current_user.has_permissions('manage_templates') %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user