mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-25 16:54:03 -04:00
Compare commits
400 Commits
81d4fc0b8c
...
06-12-25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e81f27b3b0 | ||
|
|
e1e62f3567 | ||
|
|
3e9ad2ef17 | ||
|
|
fc9686b202 | ||
|
|
63ec371db1 | ||
|
|
5bdb4cb3b5 | ||
|
|
3354b38b8e | ||
|
|
97c1093de7 | ||
|
|
dec0fb4e63 | ||
|
|
db5c74b861 | ||
|
|
7452a3675c | ||
|
|
6c07780872 | ||
|
|
87d5253bc1 | ||
|
|
518e2b5bce | ||
|
|
b1cbcace38 | ||
|
|
2648e10f42 | ||
|
|
7cd6bc2725 | ||
|
|
9293bece83 | ||
|
|
672651f3a6 | ||
|
|
888a615d03 | ||
|
|
cf7af35178 | ||
|
|
057d703851 | ||
|
|
5b0c739adb | ||
|
|
6c9e79c7a4 | ||
|
|
d53bc5ff38 | ||
|
|
e8d8bf1eb1 | ||
|
|
c60f2eab0d | ||
|
|
f8f01c99e6 | ||
|
|
81f24f08ef | ||
|
|
f4795843cc | ||
|
|
71d076e32f | ||
|
|
42aca4ebe7 | ||
|
|
9815c9ec9f | ||
|
|
6df7f3f926 | ||
|
|
6970f2f981 | ||
|
|
3156d5e7ae | ||
|
|
312cdf08fa | ||
|
|
fa158cd762 | ||
|
|
1b4c4350ce | ||
|
|
63abd500b6 | ||
|
|
31c58e0be7 | ||
|
|
37fee05bfb | ||
|
|
86f8d66573 | ||
|
|
7b6a6ffe53 | ||
|
|
396459be67 | ||
|
|
a2fab31653 | ||
|
|
1e95628c56 | ||
|
|
3672b16315 | ||
|
|
f290ae30a7 | ||
|
|
e5dc5f12aa | ||
|
|
12b8dd6c4f | ||
|
|
36c7781e37 | ||
|
|
1daccc812c | ||
|
|
19b0e24602 | ||
|
|
71b865cc12 | ||
|
|
526402a7f4 | ||
|
|
a5399ae693 | ||
|
|
e84750fbbf | ||
|
|
6d0bd1cfca | ||
|
|
a9ff570a53 | ||
|
|
9c55f5035c | ||
|
|
3c63a345c9 | ||
|
|
c5613954a0 | ||
|
|
87489c8e55 | ||
|
|
8c1c9e877d | ||
|
|
8232d4ca21 | ||
|
|
229f8467c8 | ||
|
|
47571bc342 | ||
|
|
9f6e449b5e | ||
|
|
7942e02747 | ||
|
|
f9b96e9f83 | ||
|
|
b3736b03b1 | ||
|
|
00832b9555 | ||
|
|
2926da155e | ||
|
|
9430bbd97e | ||
|
|
8b3cb14666 | ||
|
|
0d8f1e9c01 | ||
|
|
56980e04ce | ||
|
|
b990363436 | ||
|
|
1981c02207 | ||
|
|
09cf2098af | ||
|
|
4cd727ab51 | ||
|
|
15501fb6ce | ||
|
|
595ae759d1 | ||
|
|
5053d5d440 | ||
|
|
55a86bcebc | ||
|
|
91b83ab749 | ||
|
|
9042f58e2c | ||
|
|
cf6c402e66 | ||
|
|
09f1c712bb | ||
|
|
f2b53628df | ||
|
|
f1b31eb85e | ||
|
|
3e5c194a11 | ||
|
|
65d13701c7 | ||
|
|
8722ae8dd9 | ||
|
|
33ab578d1f | ||
|
|
8ca029ea4d | ||
|
|
9d737e35d7 | ||
|
|
702c8c369e | ||
|
|
b81c6b20ec | ||
|
|
7dfe9c5988 | ||
|
|
4cbcf3fe27 | ||
|
|
69c8bbc6b7 | ||
|
|
6a606a467e | ||
|
|
3d44e76383 | ||
|
|
038f4e294a | ||
|
|
9aa7138902 | ||
|
|
86cd481786 | ||
|
|
b3e09219a8 | ||
|
|
31293557ef | ||
|
|
3fe74bea54 | ||
|
|
6aa84305f6 | ||
|
|
06f0eaa3c7 | ||
|
|
5c927d009b | ||
|
|
bbd19bb513 | ||
|
|
8b2139727e | ||
|
|
e95740b52b | ||
|
|
bb90f81571 | ||
|
|
45ce6d8e8e | ||
|
|
f824549e18 | ||
|
|
dabba6b696 | ||
|
|
ba0daeaf76 | ||
|
|
9bd81fa314 | ||
|
|
f5690c3384 | ||
|
|
e950e87ded | ||
|
|
32eb6c1e8f | ||
|
|
39b9cc08a3 | ||
|
|
2d92038f53 | ||
|
|
8aa074114e | ||
|
|
e1e9ba691b | ||
|
|
665d7b543b | ||
|
|
7ba4f3658e | ||
|
|
0afc93958c | ||
|
|
d27bc213d4 | ||
|
|
589c01ed75 | ||
|
|
29db4dcf4d | ||
|
|
0ad1b09763 | ||
|
|
e8e971ed87 | ||
|
|
30785f022c | ||
|
|
776dabc750 | ||
|
|
8be4538dbe | ||
|
|
0ea2043796 | ||
|
|
0f0a593998 | ||
|
|
3924029211 | ||
|
|
69cd75b7c0 | ||
|
|
1c048e140c | ||
|
|
abf7f1b4c7 | ||
|
|
ef6eeb6e2e | ||
|
|
134dde7899 | ||
|
|
9f647e5e81 | ||
|
|
99b1ad6af3 | ||
|
|
044dc90f6e | ||
|
|
3286e87096 | ||
|
|
40ece121b1 | ||
|
|
94c465f78c | ||
|
|
067a5efdc1 | ||
|
|
db885d14ec | ||
|
|
757803312d | ||
|
|
0ec9805bcc | ||
|
|
316e8d8807 | ||
|
|
790fe2d6f9 | ||
|
|
c2c57e72d5 | ||
|
|
444800fbda | ||
|
|
cb2ad27d1c | ||
|
|
1cbcc0b150 | ||
|
|
cc6d820277 | ||
|
|
206247f543 | ||
|
|
6cc152c07c | ||
|
|
45b4cdac56 | ||
|
|
0f9836aa8b | ||
|
|
1634201150 | ||
|
|
8d880bdfec | ||
|
|
98d1519844 | ||
|
|
c00e8c9ecd | ||
|
|
0713f69ec9 | ||
|
|
26677f116f | ||
|
|
5dcf967941 | ||
|
|
e658ed2584 | ||
|
|
1b6894139d | ||
|
|
2459ecd715 | ||
|
|
8079a95c7a | ||
|
|
802bda5169 | ||
|
|
3b8b38d6ad | ||
|
|
bc2738a97a | ||
|
|
0a42b1202d | ||
|
|
a0b4a2124a | ||
|
|
3c1574d070 | ||
|
|
a7787cb919 | ||
|
|
2a4d8abc26 | ||
|
|
17c12481c9 | ||
|
|
3788fe7a91 | ||
|
|
39682c4c50 | ||
|
|
bca36e1d9b | ||
|
|
20fcf8fa3b | ||
|
|
c8904e7d70 | ||
|
|
0e724eda99 | ||
|
|
6fc16db52b | ||
|
|
23cb68e3bc | ||
|
|
d43bf48686 | ||
|
|
9d57ac0294 | ||
|
|
f8c47f0194 | ||
|
|
28f9c3d9ff | ||
|
|
e8c0f985c7 | ||
|
|
8aa7a63f55 | ||
|
|
3f944e2447 | ||
|
|
bd884012d4 | ||
|
|
ecbde5ab1f | ||
|
|
33cfbe13dd | ||
|
|
81db86b463 | ||
|
|
c5d8792161 | ||
|
|
3d07e6f898 | ||
|
|
1a1f9a2ab4 | ||
|
|
4728ee20e8 | ||
|
|
9e63f9b096 | ||
|
|
ecfde62648 | ||
|
|
be0dcd103f | ||
|
|
17544c4b98 | ||
|
|
83515cd321 | ||
|
|
d1d3a0d426 | ||
|
|
8b6edf93db | ||
|
|
f64a9a0072 | ||
|
|
0ff42e4561 | ||
|
|
80314ad76d | ||
|
|
b017a3c1e6 | ||
|
|
6193125492 | ||
|
|
0b6e52349d | ||
|
|
96cbebefed | ||
|
|
9b8d0c2b2f | ||
|
|
abb3ba64d9 | ||
|
|
be038daa0a | ||
|
|
abf822cf39 | ||
|
|
63d96d46b8 | ||
|
|
5cd703eb2f | ||
|
|
2c08d638a0 | ||
|
|
b9e08a5bc8 | ||
|
|
540ee74243 | ||
|
|
ac69799108 | ||
|
|
15dcf7474f | ||
|
|
39972c341a | ||
|
|
5887c1cf5c | ||
|
|
35176328ca | ||
|
|
e64446f5be | ||
|
|
a9901fc951 | ||
|
|
eb3780e5b6 | ||
|
|
d5131c61f9 | ||
|
|
b9c50c1e5d | ||
|
|
000b2b7819 | ||
|
|
34ed31d611 | ||
|
|
c844a6bed8 | ||
|
|
566228d051 | ||
|
|
f79bb3d95a | ||
|
|
c87a5d6bc6 | ||
|
|
7fef3fff26 | ||
|
|
eff92773cc | ||
|
|
f771f2374d | ||
|
|
0e48ed2716 | ||
|
|
149d8e3ce5 | ||
|
|
e5dce090e6 | ||
|
|
4b138711e2 | ||
|
|
824364f2b6 | ||
|
|
50bdf916ba | ||
|
|
285bee3809 | ||
|
|
8e28cdbb56 | ||
|
|
02023aa775 | ||
|
|
2adb87dfe4 | ||
|
|
43548a823e | ||
|
|
db4c02532a | ||
|
|
598e5eca91 | ||
|
|
b48377e4b4 | ||
|
|
ccdfd0c10e | ||
|
|
0917ddaa23 | ||
|
|
7fbed553aa | ||
|
|
0710372db4 | ||
|
|
db7d59c8c8 | ||
|
|
c5228d9557 | ||
|
|
eb2d4798bd | ||
|
|
3ed99ccd45 | ||
|
|
b91e687d17 | ||
|
|
5eb08b8042 | ||
|
|
aa59ac1d9c | ||
|
|
c825878c45 | ||
|
|
e6b76486f4 | ||
|
|
69ae1ea0e1 | ||
|
|
b468620ead | ||
|
|
4279467b16 | ||
|
|
f0ff50eb63 | ||
|
|
86b4547499 | ||
|
|
a6145bec19 | ||
|
|
1c795253d4 | ||
|
|
6c16482577 | ||
|
|
bf03391b6d | ||
|
|
e04806b1eb | ||
|
|
7410a00519 | ||
|
|
fb20ea6e1c | ||
|
|
cd40580231 | ||
|
|
b78a430ef9 | ||
|
|
7a52290bb7 | ||
|
|
4b5fee0bba | ||
|
|
350526f4ad | ||
|
|
fb5f7d969e | ||
|
|
2d6afb7904 | ||
|
|
670d82b72c | ||
|
|
ec7af325df | ||
|
|
23782a83d0 | ||
|
|
609c472cff | ||
|
|
386b3f8348 | ||
|
|
8bf58cdca9 | ||
|
|
6b92941156 | ||
|
|
0ccbe17b3b | ||
|
|
ae305227bb | ||
|
|
7130925b9a | ||
|
|
f484a9c432 | ||
|
|
6f6402e36a | ||
|
|
714e5011c3 | ||
|
|
6bfa9ede76 | ||
|
|
57023b3a3a | ||
|
|
0e31d34672 | ||
|
|
1b9e19072e | ||
|
|
55dc8e31ca | ||
|
|
f9a08dffe0 | ||
|
|
f661ea39ca | ||
|
|
431dbb10dd | ||
|
|
25524324c5 | ||
|
|
5abc0a4c8e | ||
|
|
3328853bd0 | ||
|
|
d6baaebef8 | ||
|
|
b6a6ffb2ab | ||
|
|
b01a62225e | ||
|
|
522340f52b | ||
|
|
fe42f5ef64 | ||
|
|
b4afcb07aa | ||
|
|
5d3364a114 | ||
|
|
70b149476c | ||
|
|
4a1ac9b85c | ||
|
|
10c61440c9 | ||
|
|
34fdf7a1ca | ||
|
|
20cbb0a8db | ||
|
|
14024a28ca | ||
|
|
280e348fa6 | ||
|
|
72f36a4d15 | ||
|
|
0b866aa7a7 | ||
|
|
77e9475ed8 | ||
|
|
b4e239c9bb | ||
|
|
c016530a7d | ||
|
|
2779e398f6 | ||
|
|
8dfa881f26 | ||
|
|
5f258cb699 | ||
|
|
a53df8998c | ||
|
|
e3843f33c6 | ||
|
|
20e0dc6c3e | ||
|
|
e9029eef09 | ||
|
|
d0d6113a05 | ||
|
|
232c6cb9e1 | ||
|
|
ed238bfe6f | ||
|
|
a3eeeb2b2c | ||
|
|
54d8996e2a | ||
|
|
88e9f1dec6 | ||
|
|
7de1a9b944 | ||
|
|
b4a8eb7210 | ||
|
|
f975953aa7 | ||
|
|
34b39acf38 | ||
|
|
3e0e290dc0 | ||
|
|
8c32af4ee1 | ||
|
|
5464da155a | ||
|
|
01678ce67a | ||
|
|
67b7dc28b4 | ||
|
|
d4a2584dfb | ||
|
|
638a4f20da | ||
|
|
fbd70b7c38 | ||
|
|
06ce617f0a | ||
|
|
1afe8121b8 | ||
|
|
9d7b6d3147 | ||
|
|
7b2d031591 | ||
|
|
64ac382af9 | ||
|
|
ca1e87a54b | ||
|
|
da695557b0 | ||
|
|
3e18c8f595 | ||
|
|
6d398f7301 | ||
|
|
e96f01f4a9 | ||
|
|
f8eb512aab | ||
|
|
6dd44fdc2d | ||
|
|
d8ca53e08b | ||
|
|
bea926f103 | ||
|
|
6f8f7346ca | ||
|
|
8dbcc59726 | ||
|
|
2ca7f5d2b0 | ||
|
|
f8c7f6b817 | ||
|
|
193184983d | ||
|
|
38451d77ad | ||
|
|
032b9927aa | ||
|
|
06c238269f | ||
|
|
2064fc864a | ||
|
|
f55d47e3d1 | ||
|
|
7813ce7914 | ||
|
|
97b8f70c1f | ||
|
|
21980d2bdf | ||
|
|
f246cc7c51 | ||
|
|
e0e8c56a61 | ||
|
|
d79ece2d01 | ||
|
|
606b124912 |
66
.ds.baseline
66
.ds.baseline
@@ -127,6 +127,16 @@
|
||||
}
|
||||
],
|
||||
"results": {
|
||||
".github/workflows/checks.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 68,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/assets/js/uswds.min.js": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
@@ -151,7 +161,7 @@
|
||||
"filename": "app/config.py",
|
||||
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
|
||||
"is_verified": false,
|
||||
"line_number": 118,
|
||||
"line_number": 123,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -389,26 +399,6 @@
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/templates/new/components/head.html": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/templates/new/components/head.html",
|
||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||
"is_verified": false,
|
||||
"line_number": 33,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/templates/old/admin_template.html": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/templates/old/admin_template.html",
|
||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||
"is_verified": false,
|
||||
"line_number": 18,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"deploy-config/sandbox.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
@@ -563,7 +553,7 @@
|
||||
"filename": "tests/app/main/views/test_sign_in.py",
|
||||
"hashed_secret": "8b8b69116ee882b5e987e330f55db81aba0636f9",
|
||||
"is_verified": false,
|
||||
"line_number": 97,
|
||||
"line_number": 95,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@@ -642,37 +632,7 @@
|
||||
"line_number": 3266,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py",
|
||||
"hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/encryption/test_encryption_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py",
|
||||
"hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605",
|
||||
"is_verified": false,
|
||||
"line_number": 13,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py",
|
||||
"hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-03-20T18:22:36Z"
|
||||
"generated_at": "2025-06-04T16:12:20Z"
|
||||
}
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
12
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
@@ -9,10 +9,9 @@ body:
|
||||
label: Log In
|
||||
options:
|
||||
- label: notify-staging.app.cloud.gov resolves
|
||||
- label: Clicking sign in takes you to login page
|
||||
- label: 2FA code is sent promptly
|
||||
- label: 2FA code input screen appears as expected
|
||||
- label: After code is entered, you are brought to the “choose service” screen
|
||||
- label: Clicking sign in takes you to Login.gov
|
||||
- label: Can sign in via Login.gov
|
||||
- label: After successful login, you are brought to the “choose service” screen
|
||||
- label: Choosing a service brings you to the dashboard for that service
|
||||
|
||||
- type: checkboxes
|
||||
@@ -72,11 +71,10 @@ body:
|
||||
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that person’s profile
|
||||
- label: Clicking on “Remove this team member” prompts a confirmation
|
||||
- label: Clicking the confirmation removes the team member
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: sign_out
|
||||
attributes:
|
||||
label: Sign Out
|
||||
options:
|
||||
- label: Hitting “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
- label: Clicking “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
12
.github/actions/setup-project/action.yml
vendored
12
.github/actions/setup-project/action.yml
vendored
@@ -9,13 +9,19 @@ runs:
|
||||
sudo apt-get update \
|
||||
&& sudo apt-get install -y --no-install-recommends \
|
||||
libcurl4-openssl-dev
|
||||
- name: Set up Python 3.12.3
|
||||
- name: Set up Python 3.12.2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.3"
|
||||
python-version: "3.12.2"
|
||||
- name: Install poetry
|
||||
shell: bash
|
||||
run: pip install poetry==1.8.5
|
||||
run: pip install poetry==2.1.3
|
||||
- name: Install poetry export plugin
|
||||
shell: bash
|
||||
run: poetry self add poetry-plugin-export
|
||||
- name: Downgrade virtualenv to compatible version
|
||||
shell: bash
|
||||
run: pip install "virtualenv<20.30"
|
||||
- name: Install application dependencies
|
||||
shell: bash
|
||||
run: make bootstrap
|
||||
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -10,9 +10,9 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "A-Shumway42"
|
||||
- "alexjanousekGSA"
|
||||
reviewers:
|
||||
- "A-Shumway42"
|
||||
- "alexjanousekGSA"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
131
.github/workflows/checks.yml
vendored
131
.github/workflows/checks.yml
vendored
@@ -51,74 +51,73 @@ jobs:
|
||||
- name: Check coverage threshold
|
||||
run: poetry run coverage report --fail-under=90
|
||||
|
||||
# TODO FIX THIS!
|
||||
# end-to-end-tests:
|
||||
# if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
end-to-end-tests:
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
|
||||
# permissions:
|
||||
# checks: write
|
||||
# pull-requests: write
|
||||
# contents: write
|
||||
# runs-on: ubuntu-latest
|
||||
# environment: staging
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres
|
||||
# env:
|
||||
# POSTGRES_USER: user
|
||||
# POSTGRES_PASSWORD: password
|
||||
# POSTGRES_DB: test_notification_api
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# # Maps tcp port 5432 on service container to the host
|
||||
# - 5432:5432
|
||||
# redis:
|
||||
# image: redis
|
||||
# options: >-
|
||||
# --health-cmd "redis-cli ping"
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# # Maps tcp port 6379 on service container to the host
|
||||
# - 6379:6379
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: test_notification_api
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 6379 on service container to the host
|
||||
- 6379:6379
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: ./.github/actions/setup-project
|
||||
# - uses: jwalton/gh-find-current-pr@v1
|
||||
# id: findPr
|
||||
# - name: Check API Server availability
|
||||
# run: |
|
||||
# curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
# - name: Run Admin server
|
||||
# # If we want to log stuff and see what's broken,
|
||||
# # insert this line:
|
||||
# # tail -f admin-server.log &
|
||||
# # above make e2e-test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- name: Check API Server availability
|
||||
run: |
|
||||
curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
- name: Run Admin server
|
||||
# If we want to log stuff and see what's broken,
|
||||
# insert this line:
|
||||
# tail -f admin-server.log &
|
||||
# above make e2e-test
|
||||
|
||||
|
||||
# run: |
|
||||
# make run-flask > admin-server.log 2>&1 &
|
||||
# tail -f admin-server.log &
|
||||
# make e2e-test
|
||||
run: |
|
||||
make run-flask > admin-server.log 2>&1 &
|
||||
tail -f admin-server.log &
|
||||
make e2e-test
|
||||
|
||||
# env:
|
||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
# SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
# DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
# ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
# ADMIN_CLIENT_USERNAME: notify-admin
|
||||
# NOTIFY_ENVIRONMENT: e2etest
|
||||
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
# NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
# VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
env:
|
||||
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: notify-admin
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -140,13 +139,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
PYSEC-2024-60
|
||||
PYSEC-2022-43162
|
||||
PYSEC-2023-312
|
||||
- name: Run npm audit
|
||||
run: make npm-audit
|
||||
|
||||
@@ -167,6 +165,7 @@ jobs:
|
||||
run: make run-flask &
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: scanning
|
||||
FEATURE_SOCKET_ENABLED: true
|
||||
- name: Run OWASP Baseline Scan
|
||||
uses: zaproxy/action-baseline@v0.14.0
|
||||
with:
|
||||
|
||||
4
.github/workflows/daily_checks.yml
vendored
4
.github/workflows/daily_checks.yml
vendored
@@ -25,10 +25,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
PYSEC-2023-312
|
||||
- name: Run npm audit
|
||||
run: make npm-audit
|
||||
|
||||
|
||||
6
.github/workflows/deploy-demo.yml
vendored
6
.github/workflows/deploy-demo.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
@@ -63,6 +63,8 @@ jobs:
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-demo.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
|
||||
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -87,6 +89,8 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var API_PUBLIC_URL="$API_PUBLIC_URL"
|
||||
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
|
||||
--strategy rolling
|
||||
|
||||
- name: Deploy egress proxy
|
||||
|
||||
6
.github/workflows/deploy-prod.yml
vendored
6
.github/workflows/deploy-prod.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: cloud-gov/cg-cli-tools@main
|
||||
@@ -63,6 +63,8 @@ jobs:
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://beta.notify.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
|
||||
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -87,6 +89,8 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var API_PUBLIC_URL="$API_PUBLIC_URL"
|
||||
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
|
||||
--strategy rolling
|
||||
|
||||
- name: Deploy egress proxy
|
||||
|
||||
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
run: poetry export --output requirements.txt
|
||||
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
@@ -69,6 +69,8 @@ jobs:
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
|
||||
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -93,6 +95,8 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var API_PUBLIC_URL="$API_PUBLIC_URL"
|
||||
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
|
||||
--strategy rolling
|
||||
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@
|
||||
*.XLSX
|
||||
|
||||
## Non user files allowed to be commited
|
||||
!app/assets/pdf/wa_case_study.pdf
|
||||
!app/assets/pdf/best-practices-for-texting-the-public.pdf
|
||||
!app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf
|
||||
!app/assets/pdf/best-practices-section-outline.pdf
|
||||
|
||||
2
.profile
2
.profile
@@ -6,4 +6,4 @@
|
||||
export NEW_RELIC_PROXY_HOST=$egress_proxy
|
||||
export http_proxy=$egress_proxy
|
||||
export https_proxy=$egress_proxy
|
||||
export no_proxy="apps.internal"
|
||||
export no_proxy="apps.internal,s3-fips.us-gov-west-1.amazonaws.com"
|
||||
|
||||
30
Makefile
30
Makefile
@@ -6,6 +6,7 @@ APP_VERSION_FILE = app/version.py
|
||||
|
||||
GIT_BRANCH ?= $(shell git symbolic-ref --short HEAD 2> /dev/null || echo "detached")
|
||||
GIT_COMMIT ?= $(shell git rev-parse HEAD 2> /dev/null || echo "")
|
||||
GIT_HOOKS_PATH ?= $(shell git config --global core.hooksPath || echo "")
|
||||
|
||||
VIRTUALENV_ROOT := $(shell [ -z $$VIRTUAL_ENV ] && echo $$(pwd)/venv || echo $$VIRTUAL_ENV)
|
||||
|
||||
@@ -14,16 +15,27 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
|
||||
## DEVELOPMENT
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: generate-version-file ## Set up everything to run the app
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
bootstrap: ## Set up everything to run the app
|
||||
make generate-version-file
|
||||
poetry sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
poetry run pre-commit install
|
||||
source $(NVMSH) --no-use && nvm install && npm install
|
||||
source $(NVMSH) && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: bootstrap-with-git-hooks
|
||||
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
|
||||
make generate-version-file
|
||||
poetry sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
git config --global --unset-all core.hooksPath
|
||||
poetry run pre-commit install
|
||||
git config --global core.hookspath "${GIT_HOOKS_PATH}"
|
||||
source $(NVMSH) --no-use && nvm install && npm install
|
||||
source $(NVMSH) && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: watch-frontend
|
||||
watch-frontend: ## Build frontend and watch for changes
|
||||
source $(NVMSH) && npm run watch
|
||||
@@ -53,7 +65,7 @@ generate-version-file: ## Generates the app version file
|
||||
@echo -e "__git_commit__ = \"${GIT_COMMIT}\"\n__time__ = \"${DATE}\"" > ${APP_VERSION_FILE}
|
||||
|
||||
.PHONY: test
|
||||
test: py-lint py-test js-lint js-test ## Run tests
|
||||
test: py-lint py-test js-test ## Run tests
|
||||
|
||||
.PHONY: py-lint
|
||||
py-lint: ## Run python linting scanners and black
|
||||
@@ -83,18 +95,18 @@ too-complex:
|
||||
py-test: export NEW_RELIC_ENVIRONMENT=test
|
||||
py-test: ## Run python unit tests
|
||||
poetry run coverage run -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
poetry run coverage report --fail-under=96
|
||||
poetry run coverage report --fail-under=93
|
||||
poetry run coverage html -d .coverage_cache
|
||||
|
||||
.PHONY: dead-code
|
||||
dead-code: ## 60% is our aspirational goal, but currently breaks the build
|
||||
poetry run vulture ./app ./notifications_utils --min-confidence=100
|
||||
|
||||
|
||||
.PHONY: e2e-test
|
||||
e2e-test: export NEW_RELIC_ENVIRONMENT=test
|
||||
e2e-test: ## Run end-to-end integration tests; note that --browser webkit isn't currently working
|
||||
DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
|
||||
@echo "Running E2E tests in path: $${TESTPATH:-tests/end_to_end}"
|
||||
@bash -c 'DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox "$${TESTPATH:-tests/end_to_end}"'
|
||||
|
||||
.PHONY: js-lint
|
||||
js-lint: ## Run javascript linting scanners
|
||||
@@ -115,7 +127,7 @@ py-lock: ## Syncs dependencies and updates lock file without performing recursiv
|
||||
|
||||
.PHONY: freeze-requirements
|
||||
freeze-requirements: ## create static requirements.txt
|
||||
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
poetry export --output requirements.txt
|
||||
|
||||
.PHONY: pip-audit
|
||||
pip-audit:
|
||||
|
||||
18
README.md
18
README.md
@@ -31,7 +31,7 @@ You will need the following items:
|
||||
[setting up an account](https://cloud.gov/sign-up/) (requires a `.mil`,
|
||||
`.gov`, or `.fed.us` email address) and getting access to the
|
||||
`notify-local-dev` and `notify-staging` spaces.
|
||||
- Admin priviliges and SSH access on your machine; you may need to work with
|
||||
- Admin privileges and SSH access on your machine; you may need to work with
|
||||
your organization's IT support staff if you're not sure or don't currently
|
||||
have this access.
|
||||
|
||||
@@ -308,6 +308,22 @@ you'll be set with an upgraded version of Python.
|
||||
|
||||
_If you're not sure about the details of your current virtual environment, you can run `poetry env info` to get more information. If you've been using `pyenv` for everything, you can also see all available virtual environments with `pyenv virtualenvs`._
|
||||
|
||||
|
||||
#### Poetry upgrades ####
|
||||
|
||||
If you are doing a new project setup, then after you install poetry you need to install the export plugin
|
||||
|
||||
```sh
|
||||
poetry self add poetry-plugin-export
|
||||
```
|
||||
|
||||
If you are upgrading from poetry 1.8.5, you need to do this:
|
||||
|
||||
```sh
|
||||
curl -sSL https://install.python-poetry.org | python3 - --version 2.1.3
|
||||
poetry self add poetry-export-plugin
|
||||
```
|
||||
|
||||
#### Updating the .env file for Login.gov
|
||||
|
||||
To configure the application for Login.gov, you will need to update the following environment variables in the .env file:
|
||||
|
||||
@@ -109,6 +109,7 @@ from app.notify_client.template_statistics_api_client import template_statistics
|
||||
from app.notify_client.upload_api_client import upload_api_client
|
||||
from app.notify_client.user_api_client import user_api_client
|
||||
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
|
||||
from app.utils.api_health import is_api_down
|
||||
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
||||
from notifications_utils import logging, request_helper
|
||||
from notifications_utils.formatters import (
|
||||
@@ -140,6 +141,9 @@ navigation = {
|
||||
def _csp(config):
|
||||
asset_domain = config["ASSET_DOMAIN"]
|
||||
logo_domain = config["LOGO_CDN_DOMAIN"]
|
||||
api_public_url = config["API_PUBLIC_URL"]
|
||||
api_public_ws_url = config["API_PUBLIC_WS_URL"]
|
||||
|
||||
return {
|
||||
"default-src": ["'self'", asset_domain],
|
||||
"frame-src": [
|
||||
@@ -158,11 +162,14 @@ def _csp(config):
|
||||
"https://www.googletagmanager.com",
|
||||
"https://www.google-analytics.com",
|
||||
"https://dap.digitalgov.gov",
|
||||
"https://cdn.socket.io",
|
||||
],
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.google-analytics.com",
|
||||
f"{api_public_url}",
|
||||
f"{api_public_ws_url}",
|
||||
],
|
||||
"style-src": ["'self'", asset_domain],
|
||||
"img-src": ["'self'", asset_domain, logo_domain],
|
||||
@@ -173,17 +180,22 @@ def create_app(application):
|
||||
@application.after_request
|
||||
def add_csp_header(response):
|
||||
existing_csp = response.headers.get("Content-Security-Policy", "")
|
||||
response.headers["Content-Security-Policy"] = existing_csp + "; form-action 'self';"
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
existing_csp + "; form-action 'self';"
|
||||
)
|
||||
return response
|
||||
# @application.context_processor
|
||||
# def inject_feature_flags():
|
||||
# this is where feature flags can be easily added as a dictionary within context
|
||||
# feature_about_page_enabled = application.config.get(
|
||||
# "FEATURE_ABOUT_PAGE_ENABLED", False
|
||||
# )
|
||||
# return dict(
|
||||
# FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled,
|
||||
# )
|
||||
|
||||
@application.context_processor
|
||||
def inject_feature_flags():
|
||||
# this is where feature flags can be easily added as a dictionary within context
|
||||
feature_socket_enabled = application.config.get("FEATURE_SOCKET_ENABLED", False)
|
||||
return dict(
|
||||
FEATURE_SOCKET_ENABLED=feature_socket_enabled,
|
||||
)
|
||||
|
||||
@application.context_processor
|
||||
def inject_is_api_down():
|
||||
return {"is_api_down": is_api_down()}
|
||||
|
||||
@application.context_processor
|
||||
def inject_initial_signin_url():
|
||||
|
||||
1
app/assets/images/api-error.svg
Normal file
1
app/assets/images/api-error.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m468.547 144.82h-63.047v-121.32c0-12.958-10.542-23.5-23.5-23.5h-324c-12.958 0-23.5 10.542-23.5 23.5v84.833c.344 9.931 14.659 9.923 15 0v-33.333h341v69.82h-121.998c-14.312 0-25.954 11.574-25.954 25.8v83.174l-28.866 19.221c-5.511 3.444-5.51 12.168.001 15.611l28.865 19.22v83.174c0 14.226 11.643 25.799 25.954 25.799h121.998v71.68c0 4.687-3.813 8.5-8.5 8.5h-324c-4.687 0-8.5-3.813-8.5-8.5v-20h131.5c7.444 0 13.5-6.056 13.5-13.5v-127.999c0-7.444-6.056-13.5-13.5-13.5h-131.5v-168.021c-.344-9.931-14.659-9.923-15 0v168.021h-3.5c-7.444 0-13.5 6.056-13.5 13.5v128c0 7.444 6.056 13.5 13.5 13.5h3.5v20c0 12.958 10.542 23.5 23.5 23.5h324c12.958 0 23.5-10.542 23.5-23.5v-71.68h11.5c9.931-.344 9.923-14.659 0-15h-148.498c-6.04 0-10.954-4.845-10.954-10.799v-85.173c0-3.779-1.882-7.288-5.031-9.384l-23.492-15.643 23.492-15.644c3.15-2.098 5.031-5.606 5.031-9.384v-85.173c0-5.955 4.914-10.8 10.954-10.8h200.045c6.04 0 10.953 4.845 10.953 10.8v220.401c0 5.955-4.913 10.799-10.953 10.799h-16.436c-9.931.344-9.923 14.659 0 15h16.436c14.311 0 25.953-11.574 25.953-25.799v-220.401c0-14.226-11.643-25.8-25.953-25.8zm-256.047-129.82v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.333 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.334 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-52.333 45v-36.5c0-4.687 3.813-8.5 8.5-8.5h13.833v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.334v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.333v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h154.5c4.687 0 8.5 3.813 8.5 8.5v36.5zm-17 268.5h147v125h-147z"/><path d="m451.356 344.514c2.439-4.507 2.214-9.973-.589-14.264l-69.093-105.771c-5.242-8.361-18.107-8.359-23.35 0l-69.092 105.771c-2.803 4.291-3.028 9.756-.589 14.263s7.139 7.307 12.264 7.307h138.186c5.125 0 9.824-2.799 12.263-7.306zm-148.499-7.694 67.143-102.786 67.143 102.786z"/><path d="m362.5 266.32v31.445c.344 9.931 14.659 9.923 15 0v-31.445c-.344-9.931-14.659-9.923-15 0z"/><path d="m370 310.82c-4.143 0-7.5 3.358-7.5 7.5v2.5c.344 9.931 14.659 9.923 15 0v-2.5c0-4.142-3.357-7.5-7.5-7.5z"/><path d="m91.375 123.094h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 149.316h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 175.539h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m91.375 225.76h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 251.982h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 278.205h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m139.354 356.038c-2.93-2.929-7.678-2.929-10.607 0l-22.747 22.747-22.747-22.747c-2.93-2.929-7.678-2.929-10.607 0s-2.929 7.678 0 10.606l22.748 22.748-22.748 22.748c-2.929 2.929-2.929 7.678 0 10.606 2.93 2.929 7.677 2.929 10.607 0l22.747-22.747 22.747 22.747c2.93 2.929 7.677 2.929 10.607 0 2.929-2.929 2.929-7.678 0-10.606l-22.748-22.748 22.748-22.748c2.929-2.928 2.929-7.677 0-10.606z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
17
app/assets/images/messaging-devices.svg
Normal file
17
app/assets/images/messaging-devices.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="305" height="304" viewBox="0 0 381 380" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M163.41 47.1289H32.7852C14.983 47.1289 0.5 61.6119 0.5 79.414V293.793C0.755313 301.164 11.3797 301.158 11.6328 293.793V79.414C11.6328 67.7506 21.1217 58.2617 32.7852 58.2617H163.41C175.074 58.2617 184.562 67.7506 184.562 79.414V335.84H11.6328V319.851C11.3775 312.481 0.753086 312.487 0.5 319.851V346.601C0.5 364.403 14.983 378.887 32.7852 378.887H163.41C181.212 378.887 195.695 364.403 195.695 346.601V79.414C195.695 61.6119 181.212 47.1289 163.41 47.1289ZM163.41 367.754H32.7852C21.2464 367.754 11.8414 358.464 11.6425 346.972H184.554C184.354 358.464 174.949 367.754 163.41 367.754Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M72.8629 102.793H123.332C145.461 101.951 145.444 70.2287 123.332 69.3945H72.8629C50.7338 70.2362 50.7509 101.959 72.8629 102.793ZM72.8629 80.5273H123.332C130.701 80.7893 130.696 91.4004 123.332 91.6601H72.8629C65.4937 91.3981 65.4989 80.7871 72.8629 80.5273Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M41.6899 265.26V303.853C41.6899 308.56 45.5189 312.389 50.2251 312.389H145.225C149.931 312.389 153.76 308.56 153.76 303.853V265.26C153.76 260.554 149.931 256.725 145.225 256.725H50.2251C45.5181 256.725 41.6899 260.554 41.6899 265.26ZM52.8228 267.857H142.627V301.256H52.8228V267.857Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M374.934 50.0976C378.008 50.0976 380.5 47.6054 380.5 44.5312V21.4203C380.5 9.60909 370.835 0 358.955 0H258.626C246.746 0 237.081 9.60909 237.081 21.4203V99.174L211.672 116.104C207.446 118.746 207.447 125.434 211.673 128.076L237.081 145.005V195.298C237.081 207.109 246.746 216.718 258.626 216.718H358.955C370.835 216.719 380.5 207.109 380.5 195.298V71.9098C380.245 64.5391 369.62 64.545 369.367 71.9098V195.298C369.367 200.971 364.697 205.586 358.955 205.586H258.626C252.885 205.586 248.214 200.971 248.214 195.298V143.773C248.214 140.812 246.74 138.063 244.272 136.418L222.766 122.09L244.272 107.761C246.741 106.116 248.214 103.367 248.214 100.407V21.4203C248.214 15.7477 252.885 11.1328 258.626 11.1328H358.955C364.697 11.1328 369.367 15.7477 369.367 21.4203V44.5312C369.367 47.6054 371.86 50.0976 374.934 50.0976Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 44.4072H313.172C320.542 44.1519 320.537 33.5275 313.172 33.2744H279.025C271.655 33.5305 271.66 44.1549 279.025 44.4072Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 116.647H313.172C320.542 116.392 320.537 105.768 313.172 105.515H279.025C271.655 105.77 271.66 116.394 279.025 116.647Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 62.4678H336.689C344.059 62.2125 344.054 51.588 336.689 51.335H279.025C271.655 51.5903 271.66 62.2147 279.025 62.4678Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M279.025 80.5273H336.689C344.059 80.272 344.054 69.6476 336.689 69.3945H279.025C271.655 69.6498 271.66 80.2743 279.025 80.5273Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M98.0967 205.319C108.123 205.319 116.28 197.161 116.28 187.135C115.366 163.045 80.8237 163.052 79.9131 187.135C79.9131 197.161 88.0705 205.319 98.0967 205.319ZM98.0967 180.084C101.984 180.084 105.147 183.248 105.147 187.135C104.793 196.476 91.3992 196.473 91.0459 187.135C91.0459 183.247 94.2091 180.084 98.0967 180.084Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M49.0186 181.389C49.7927 183.69 51.1932 185.667 53.0569 187.135C51.1932 188.603 49.7935 190.58 49.0186 192.88C47.9662 196.005 48.1948 199.354 49.6621 202.308L56.7158 216.509C59.7425 222.603 67.1644 225.101 73.2592 222.078L77.814 219.82V224.885C77.814 231.693 83.3529 237.233 90.1618 237.233H106.039C112.844 237.233 118.38 231.697 118.38 224.891V219.82L122.935 222.078C129.032 225.101 136.452 222.603 139.479 216.509L146.533 202.305C148 199.352 148.229 196.004 147.177 192.879C146.403 190.578 145.001 188.601 143.138 187.134C147.636 183.694 149.157 177.013 146.532 171.96L139.48 157.763C136.453 151.668 129.029 149.168 122.932 152.192L118.38 154.449V149.378C118.38 142.573 112.844 137.036 106.039 137.036H90.1618C83.3537 137.036 77.814 142.575 77.814 149.384V154.449L73.2622 152.192C67.1651 149.168 59.7417 151.667 56.7144 157.762L49.6621 171.96C48.1948 174.916 47.9662 178.264 49.0186 181.389ZM59.6319 176.914L66.6842 162.717C66.9825 162.115 67.7151 161.867 68.3162 162.167L78.764 167.347C80.9646 168.437 83.5259 168.316 85.6129 167.021C87.7007 165.726 88.9468 163.486 88.9468 161.03V149.385C88.9468 148.715 89.4916 148.17 90.1618 148.17H106.039C106.705 148.17 107.247 148.712 107.247 149.379V161.03C107.247 163.486 108.493 165.726 110.581 167.021C112.668 168.316 115.228 168.439 117.43 167.346L127.878 162.166C128.48 161.868 129.211 162.115 129.51 162.716L136.563 176.913C136.747 177.284 136.698 177.62 136.626 177.836C136.553 178.053 136.388 178.349 136.016 178.533L131.411 180.817C126.23 183.267 126.231 191.002 131.411 193.451L136.019 195.736C136.39 195.92 136.554 196.215 136.626 196.431C136.699 196.647 136.747 196.982 136.563 197.351L129.509 211.555C129.211 212.154 128.48 212.401 127.881 212.103L117.43 206.921C115.229 205.83 112.668 205.952 110.582 207.247C108.494 208.542 107.248 210.782 107.248 213.239V224.89C107.248 225.556 106.706 226.099 106.039 226.099H90.1625C89.4931 226.099 88.9476 225.554 88.9476 224.884V213.239C88.9476 210.782 87.7014 208.542 85.6137 207.247C83.5229 205.95 80.9601 205.832 78.764 206.922L68.3133 212.104C67.7121 212.401 66.9833 212.155 66.6864 211.556L59.6326 197.356C59.4486 196.985 59.4968 196.649 59.5696 196.432C59.6423 196.215 59.807 195.919 60.1781 195.735L64.7842 193.451C69.9654 191.001 69.9639 183.266 64.7842 180.818L60.1789 178.534C59.8078 178.35 59.643 178.053 59.5703 177.837C59.4968 177.62 59.4486 177.285 59.6319 176.914Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M363.426 257.783H235.796C226.382 257.783 218.723 265.442 218.723 274.856V362.927C218.723 372.341 226.383 380 235.796 380H363.426C372.841 380 380.499 372.341 380.499 362.927V274.856C380.499 265.442 372.841 257.783 363.426 257.783ZM369.366 362.927C369.366 366.203 366.702 368.867 363.426 368.867H235.796C232.52 368.867 229.856 366.203 229.856 362.927V274.856C229.856 271.58 232.521 268.916 235.796 268.916H363.426C366.702 268.916 369.366 271.58 369.366 274.856V362.927Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M268.272 141.779V181.381C268.272 185.809 271.875 189.412 276.304 189.412H340.926C345.354 189.412 348.956 185.809 348.956 181.381V141.779C348.956 137.351 345.354 133.749 340.926 133.749H276.304C271.875 133.748 268.272 137.35 268.272 141.779ZM279.405 144.881H337.824V178.279H279.405V144.881Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M273.174 297.591C271.206 295.229 267.695 294.909 265.334 296.878L241.975 316.344C239.33 318.441 239.331 322.8 241.975 324.897L265.334 344.364C266.375 345.23 267.638 345.654 268.895 345.653C270.489 345.653 272.073 344.971 273.174 343.65C275.142 341.289 274.823 337.779 272.462 335.811L254.233 320.62L272.462 305.431C274.823 303.462 275.142 299.953 273.174 297.591Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M357.249 316.344L333.889 296.878C331.527 294.91 328.018 295.229 326.049 297.59C324.081 299.952 324.4 303.462 326.762 305.43L344.99 320.62L326.762 335.81C324.4 337.778 324.081 341.288 326.049 343.65C328.026 346.021 331.541 346.322 333.889 344.362L357.249 324.895C359.893 322.799 359.892 318.441 357.249 316.344Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
<path d="M314.73 284.336C311.892 283.154 308.634 284.495 307.451 287.333L281.496 349.626C280.314 352.464 281.656 355.723 284.493 356.905C287.26 358.079 290.59 356.775 291.772 353.908L317.727 291.616C318.91 288.778 317.569 285.519 314.73 284.336Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
@@ -1,27 +0,0 @@
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
Modules.Autofocus = function() {
|
||||
this.start = function(component) {
|
||||
var $component = $(component),
|
||||
forceFocus = $component.data('forceFocus');
|
||||
|
||||
// if the page loads with a scroll position, we can't assume the item to focus onload
|
||||
// is still where users intend to start
|
||||
if (($(window).scrollTop() > 0) && !forceFocus) { return; }
|
||||
|
||||
// See if the component itself is something we want to send focus to
|
||||
var target = $component.filter('input, textarea, select');
|
||||
|
||||
// Otherwise look inside the component to see if there are any elements
|
||||
// we want to send focus to
|
||||
if (target.length === 0) {
|
||||
target = $('input, textarea, select', $component);
|
||||
}
|
||||
|
||||
target.eq(0).trigger('focus');
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
})(window.GOVUK.Modules);
|
||||
@@ -1,29 +0,0 @@
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
let isSixDigitHex = value => value.match(/^#[0-9A-F]{6}$/i);
|
||||
let colourOrWhite = value => isSixDigitHex(value) ? value : '#FFFFFF';
|
||||
|
||||
Modules.ColourPreview = function() {
|
||||
|
||||
this.start = component => {
|
||||
|
||||
this.$input = $(component);
|
||||
|
||||
this.$input.closest('.usa-form-group').append(
|
||||
this.$preview = $('<span class="textbox-colour-preview"></span>')
|
||||
);
|
||||
|
||||
this.$input
|
||||
.on('input', this.update)
|
||||
.trigger('input');
|
||||
|
||||
};
|
||||
|
||||
this.update = () => this.$preview.css(
|
||||
'background', colourOrWhite(this.$input.val())
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
})(window.GOVUK.Modules);
|
||||
@@ -1,34 +1,69 @@
|
||||
function announceUploadStatusFromElement() {
|
||||
const srRegion = document.getElementById('upload-status-live');
|
||||
const success = document.getElementById('upload-success');
|
||||
const error = document.getElementById('upload-error');
|
||||
|
||||
if (!srRegion) return;
|
||||
|
||||
const message = error?.textContent || success?.textContent;
|
||||
|
||||
if (message) {
|
||||
srRegion.textContent = '';
|
||||
setTimeout(() => {
|
||||
srRegion.textContent = message + '\u00A0'; // add a non-breaking space
|
||||
srRegion.focus(); // Optional
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Exported for use in tests
|
||||
function initUploadStatusAnnouncer() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
announceUploadStatusFromElement();
|
||||
});
|
||||
}
|
||||
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
Modules.FileUpload = function() {
|
||||
|
||||
this.submit = () => this.$form.trigger('submit');
|
||||
|
||||
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
|
||||
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
|
||||
`);
|
||||
|
||||
this.start = function(component) {
|
||||
|
||||
this.$form = $(component);
|
||||
|
||||
// The label gets styled like a button and is used to hide the native file upload control. This is so that
|
||||
// users see a button that looks like the others on the site.
|
||||
|
||||
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
|
||||
|
||||
// Clear the form if the user navigates back to the page
|
||||
$(window).on("pageshow", () => this.$form[0].reset());
|
||||
|
||||
// Need to put the event on the container, not the input for it to work properly
|
||||
this.$form.on(
|
||||
'change', '.file-upload-field',
|
||||
() => this.submit() && this.showCancelButton()
|
||||
);
|
||||
|
||||
this.showCancelButton = () => {
|
||||
$('.file-upload-button', this.$form).replaceWith(`
|
||||
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
|
||||
Uploading<span class="dot-anim" aria-hidden="true"></span>
|
||||
</button>
|
||||
`);
|
||||
};
|
||||
|
||||
};
|
||||
this.start = function(component) {
|
||||
this.$form = $(component);
|
||||
|
||||
this.$form.on('click', '[data-module="upload-trigger"]', function () {
|
||||
const inputId = $(this).data('file-input-id');
|
||||
const fileInput = document.getElementById(inputId);
|
||||
if (fileInput) fileInput.click();
|
||||
});
|
||||
|
||||
$(window).on("pageshow", () => this.$form[0].reset());
|
||||
|
||||
this.$form.on('change', '.file-upload-field', () => {
|
||||
this.submit();
|
||||
this.showCancelButton();
|
||||
});
|
||||
};
|
||||
};
|
||||
})(window.GOVUK.Modules);
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
announceUploadStatusFromElement,
|
||||
initUploadStatusAnnouncer
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
initUploadStatusAnnouncer();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
this.$scrollableTable
|
||||
.on('scroll', this.toggleShadows)
|
||||
.on('scroll', this.maintainHeight)
|
||||
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
|
||||
|
||||
if (
|
||||
window.GOVUK.stickAtBottomWhenScrolling &&
|
||||
@@ -37,11 +36,11 @@
|
||||
|
||||
this.insertShims = () => {
|
||||
|
||||
const attributesForFocus = 'role aria-labelledby tabindex';
|
||||
const attributesForFocus = 'role aria-labelledby';
|
||||
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
|
||||
|
||||
this.$table.find('caption').attr('id', captionId);
|
||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`);
|
||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
|
||||
|
||||
this.$component
|
||||
.append(
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
(function(window) {
|
||||
"use strict";
|
||||
|
||||
window.GOVUK.Modules.Homepage = function() {
|
||||
|
||||
this.start = function(component) {
|
||||
|
||||
let $component = $(component);
|
||||
let iterations = 0;
|
||||
let timeout = null;
|
||||
|
||||
$component.on('click', () => {
|
||||
if (++iterations == 5) {
|
||||
$component.toggleClass('product-page-intro-wrapper--alternative');
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => iterations = 0, 1500);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
})(window);
|
||||
@@ -15,11 +15,11 @@
|
||||
|
||||
let query = normalize($searchBox.val());
|
||||
let results = 0;
|
||||
let $noResultsMessage = $('.js-live-search-no-results');
|
||||
|
||||
$targets.each(function() {
|
||||
|
||||
let content = $('.live-search-relevant', this).text() || $(this).text();
|
||||
let isMatch = normalize(content).indexOf(normalize(query)) > -1;
|
||||
|
||||
if ($(this).has(':checked').length) {
|
||||
$(this).show();
|
||||
@@ -28,17 +28,27 @@
|
||||
}
|
||||
|
||||
if (query == '') {
|
||||
$(this).css('display', '');
|
||||
$(this).removeClass('js-hidden');
|
||||
results++;
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).toggle(isMatch);
|
||||
|
||||
if (isMatch) { results++; }
|
||||
let isMatch = normalize(content).includes(normalize(query));
|
||||
|
||||
if (isMatch) {
|
||||
$(this).removeClass('js-hidden');
|
||||
results++;
|
||||
} else {
|
||||
$(this).addClass('js-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
if (query !== '' && results === 0) {
|
||||
$noResultsMessage.removeClass('js-hidden');
|
||||
} else {
|
||||
$noResultsMessage.addClass('js-hidden');
|
||||
}
|
||||
|
||||
if (state === 'loaded') {
|
||||
if (query !== '') {
|
||||
$searchBox.attr('aria-label', $searchLabel.text().trim() + ', ' + resultsSummary(results));
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
window.GOVUK.Frontend.initAll();
|
||||
|
||||
$(() => $("time.timeago").timeago());
|
||||
|
||||
var showHideContent = new GOVUK.ShowHideContent();
|
||||
showHideContent.init();
|
||||
|
||||
|
||||
100
app/assets/javascripts/notifyModal.js
Normal file
100
app/assets/javascripts/notifyModal.js
Normal file
@@ -0,0 +1,100 @@
|
||||
let activeModal = null;
|
||||
let lastFocusedElement = null;
|
||||
|
||||
function openModal(modalId) {
|
||||
const wrapper = document.getElementById(modalId);
|
||||
if (!wrapper) return;
|
||||
|
||||
const modal = wrapper.querySelector('.usa-modal, dialog');
|
||||
if (!modal) return;
|
||||
|
||||
lastFocusedElement = document.activeElement;
|
||||
|
||||
wrapper.classList.remove('is-hidden');
|
||||
modal.removeAttribute('aria-hidden');
|
||||
modal.removeAttribute('inert');
|
||||
modal.removeAttribute('hidden');
|
||||
document.body.classList.add('modal-open');
|
||||
|
||||
|
||||
// Set focus to the first focusable element inside modal
|
||||
const focusTarget = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusTarget) focusTarget.focus();
|
||||
|
||||
modal.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
activeModal = wrapper;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (!activeModal) return;
|
||||
|
||||
const modal = activeModal.querySelector('.usa-modal, dialog');
|
||||
if (modal) {
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.setAttribute('inert', '');
|
||||
modal.setAttribute('hidden', '');
|
||||
}
|
||||
|
||||
activeModal.classList.add('is-hidden');
|
||||
document.body.classList.remove('modal-open');
|
||||
|
||||
if (lastFocusedElement) lastFocusedElement.focus();
|
||||
|
||||
activeModal = null;
|
||||
|
||||
}
|
||||
|
||||
function attachModalTriggers() {
|
||||
document.querySelectorAll('[data-open-modal]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const modalId = btn.getAttribute('data-open-modal');
|
||||
openModal(modalId);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-close-modal]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Escape key closes modal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && activeModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: click outside modal closes it
|
||||
document.addEventListener('click', (e) => {
|
||||
if (activeModal && e.target.classList.contains('usa-modal-overlay')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
attachModalTriggers();
|
||||
});
|
||||
|
||||
// ✅ Check if we're in a Node.js environment (for Jest) before using `module.exports`
|
||||
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
||||
module.exports = { closeModal, openModal, attachModalTriggers };
|
||||
}
|
||||
@@ -1,37 +1,45 @@
|
||||
(function() {
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let disableSubmitButtons = function(event) {
|
||||
|
||||
var $submitButton = $(this).find(':submit');
|
||||
|
||||
if ($submitButton.data('clicked') == 'true') {
|
||||
const disableSubmitButtons = function (event) {
|
||||
const $submitButton = $(this).find(':submit');
|
||||
|
||||
if ($submitButton.data('clicked') === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
$submitButton.data('clicked', 'true');
|
||||
|
||||
$submitButton.data('clicked', 'true');
|
||||
// Add dot animation for Send/Schedule/Cancel buttons
|
||||
const buttonName = $submitButton.attr('name')?.toLowerCase();
|
||||
if (["send", "schedule", "cancel"].includes(buttonName)) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
if ($submitButton.is('[name="Send"], [name="Schedule"]')) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000);
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
// Inject dot animation span if not already present
|
||||
if ($submitButton.find('.dot-anim').length === 0) {
|
||||
$submitButton.append('<span class="dot-anim" aria-hidden="true"></span>');
|
||||
}
|
||||
|
||||
// Disable Cancel button too
|
||||
const $cancelButton = $('button[name]').filter(function () {
|
||||
return $(this).attr('name')?.toLowerCase() === 'cancel';
|
||||
});
|
||||
$cancelButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000); // fallback safety
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
}
|
||||
};
|
||||
|
||||
let renableSubmitButton = $submitButton => () => {
|
||||
|
||||
const renableSubmitButton = ($submitButton) => () => {
|
||||
$submitButton.data('clicked', '');
|
||||
$submitButton.prop('disabled', false);
|
||||
$submitButton.find('.dot-anim').remove(); // clean up if needed
|
||||
};
|
||||
|
||||
$('form').on('submit', disableSubmitButtons);
|
||||
|
||||
})();
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
|
||||
</div>
|
||||
{{/choices}}
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--done' aria-expanded='true' value='Done' />
|
||||
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
|
||||
</div>
|
||||
`),
|
||||
'chosen': Hogan.compile(`
|
||||
|
||||
83
app/assets/javascripts/socketio.js
Normal file
83
app/assets/javascripts/socketio.js
Normal file
@@ -0,0 +1,83 @@
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const isJobPage = window.location.pathname.includes('/jobs/');
|
||||
if (!isJobPage) return;
|
||||
|
||||
const jobEl = document.querySelector('[data-job-id]');
|
||||
const jobId = jobEl?.dataset?.jobId;
|
||||
const featureEnabled = jobEl?.dataset?.feature === 'true';
|
||||
const apiHost = jobEl?.dataset?.host;
|
||||
|
||||
if (!jobId) return;
|
||||
|
||||
if (featureEnabled) {
|
||||
const socket = io(apiHost);
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
console.error('Socket connect_error:', err);
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('join', { room: `job-${jobId}` });
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
socket.emit('leave', { room: `job-${jobId}` });
|
||||
});
|
||||
|
||||
const debouncedUpdate = debounce((data) => {
|
||||
updateAllJobSections();
|
||||
}, 1000);
|
||||
|
||||
socket.on('job_updated', (data) => {
|
||||
if (data.job_id !== jobId) return;
|
||||
debouncedUpdate(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllJobSections() {
|
||||
const resourceEl = document.querySelector('[data-socket-update="status"]');
|
||||
const url = resourceEl?.dataset?.resource;
|
||||
|
||||
if (!url) {
|
||||
console.warn('No resource URL found for job updates');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then((res) => res.json())
|
||||
.then(({ status, counts, notifications }) => {
|
||||
const sections = {
|
||||
status: document.querySelector('[data-socket-update="status"]'),
|
||||
counts: document.querySelector('[data-socket-update="counts"]'),
|
||||
notifications: document.querySelector(
|
||||
'[data-socket-update="notifications"]'
|
||||
),
|
||||
};
|
||||
|
||||
if (status && sections.status) {
|
||||
sections.status.innerHTML = status;
|
||||
}
|
||||
if (counts && sections.counts) {
|
||||
sections.counts.innerHTML = counts;
|
||||
}
|
||||
if (notifications && sections.notifications) {
|
||||
sections.notifications.innerHTML = notifications;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error fetching job update partials:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -197,6 +197,11 @@
|
||||
if (event.currentTarget.value === 'add-new-template' && this.$singleNotificationChannel) {
|
||||
window.location = "/services/" + this.$singleChannelService + "/templates/add-" + this.$singleNotificationChannel;
|
||||
} else {
|
||||
if (this.currentState === 'add-new-template') {
|
||||
this.$form.find('input[type=checkbox]').prop('checked', false);
|
||||
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
|
||||
}
|
||||
|
||||
if (this.stateChanged()) {
|
||||
this.render();
|
||||
}
|
||||
@@ -286,7 +291,29 @@
|
||||
mode = 'dialog';
|
||||
}
|
||||
|
||||
if (currentStateObj && ('setFocus' in currentStateObj)) {
|
||||
if (this.currentState === 'add-new-template') {
|
||||
this.$form.find('.template-list-item').addClass('js-hidden');
|
||||
$('.live-search').addClass('js-hidden');
|
||||
$('#breadcrumb-template-folders').addClass('js-hidden');
|
||||
$('#template-list').addClass('js-hidden');
|
||||
this.$form.find('input[type=checkbox]').prop('checked', false);
|
||||
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
|
||||
|
||||
$('#page-title').text('New Template');
|
||||
$('#page-description').text('Every message starts with a template. Choose to start with a blank template or copy an existing template.');
|
||||
document.title = 'New Templates';
|
||||
} else {
|
||||
this.$form.find('.template-list-item').removeClass('js-hidden');
|
||||
$('.live-search').removeClass('js-hidden');
|
||||
$('#breadcrumb-template-folders').removeClass('js-hidden');
|
||||
$('#template-list').removeClass('js-hidden');
|
||||
|
||||
$('#page-title').text('Select or create a template');
|
||||
$('#page-description').text('Every message starts with a template. To send, choose or create a template.');
|
||||
document.title = 'Select or create a template';
|
||||
}
|
||||
|
||||
if (currentStateObj && 'setFocus' in currentStateObj) {
|
||||
scrollTop = $(window).scrollTop();
|
||||
currentStateObj.setFocus();
|
||||
$(window).scrollTop(scrollTop);
|
||||
|
||||
@@ -38,6 +38,7 @@ function attachValidation() {
|
||||
const validatedRadioNames = new Set();
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.type === "hidden") return;
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
let errorElement = document.getElementById(errorId);
|
||||
|
||||
@@ -85,6 +86,7 @@ function attachValidation() {
|
||||
});
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.type === "hidden") return;
|
||||
input.addEventListener("input", function () {
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
const errorElement = document.getElementById(errorId);
|
||||
|
||||
8
app/assets/js/init.uswds.js
Normal file
8
app/assets/js/init.uswds.js
Normal file
@@ -0,0 +1,8 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.uswds && typeof window.uswds.init === 'function') {
|
||||
console.log("Calling USWDS init");
|
||||
window.uswds.init();
|
||||
} else {
|
||||
console.error("USWDS not found or init is not a function");
|
||||
}
|
||||
});
|
||||
BIN
app/assets/pdf/Studio Research & Development.pdf
Normal file
BIN
app/assets/pdf/Studio Research & Development.pdf
Normal file
Binary file not shown.
BIN
app/assets/pdf/Text Campaign Planning.pdf
Normal file
BIN
app/assets/pdf/Text Campaign Planning.pdf
Normal file
Binary file not shown.
BIN
app/assets/pdf/The Notify Story.pdf
Normal file
BIN
app/assets/pdf/The Notify Story.pdf
Normal file
Binary file not shown.
BIN
app/assets/pdf/wa_case_study.pdf
Normal file
BIN
app/assets/pdf/wa_case_study.pdf
Normal file
Binary file not shown.
@@ -344,3 +344,43 @@ h2.recipient-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usa-search__input, [type=search] {
|
||||
border-right: 1px solid;
|
||||
height: 40px;
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.usa-search .search-form__button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
// Button ellipses loading
|
||||
.dot-anim {
|
||||
display: inline-block;
|
||||
margin-left: 0; /* remove left margin if it exists */
|
||||
padding-left: 0;
|
||||
font-size: 1em;
|
||||
animation: dots 1s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
/* Optional: reduce spacing by removing whitespace node */
|
||||
button span.dot-anim {
|
||||
margin-left: 0; /* forces no space even if white-space exists */
|
||||
}
|
||||
|
||||
.dot-anim::after {
|
||||
content: '.';
|
||||
animation: dotPulse 1.5s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dotPulse {
|
||||
0% { content: ''; }
|
||||
33% { content: '.'; }
|
||||
66% { content: '..'; }
|
||||
100% { content: '...'; }
|
||||
}
|
||||
|
||||
.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -179,10 +179,19 @@ td.table-empty-message {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.user-list-item {
|
||||
border: 2px solid color('gray-5');
|
||||
padding: units(2);
|
||||
button, .usa-accordion__content {
|
||||
background-color: white;
|
||||
border: 2px solid color('gray-5');
|
||||
}
|
||||
button:hover, button[aria-expanded="true"] {
|
||||
background-color: color('gray-5');
|
||||
}
|
||||
@include at-media(desktop) {
|
||||
width: calc(50% - units(1));
|
||||
margin-bottom: 1rem;
|
||||
@@ -199,7 +208,6 @@ td.table-empty-message {
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: size('body', 'sm');
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -221,6 +229,10 @@ td.table-empty-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.js-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.folder-heading a.folder-heading-folder,
|
||||
.template-list-folder {
|
||||
background: url(../img/material-icons/folder.svg) no-repeat;
|
||||
@@ -289,6 +301,7 @@ td.table-empty-message {
|
||||
margin-top: units(1);
|
||||
width: 100%;
|
||||
border: 1px solid color('gray-60');
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -862,6 +875,10 @@ $do-dont-top-bar-width: 1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.bg-gray-cool {
|
||||
background-color: #dfe1e2;
|
||||
}
|
||||
|
||||
.svg-circle-icon {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@forward "uswds-theme";
|
||||
@forward "uswds";
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@forward "legacy-styles";
|
||||
@forward "main";
|
||||
@forward "data-visualization";
|
||||
|
||||
@@ -37,6 +37,7 @@ $path: '/static/images/';
|
||||
// Custom overrides
|
||||
.govuk-link {
|
||||
font-weight: bold;
|
||||
color: #005ea5;
|
||||
}
|
||||
|
||||
// Specific to this application
|
||||
|
||||
@@ -12,6 +12,9 @@ class Config(object):
|
||||
NOTIFY_APP_NAME = "admin"
|
||||
NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development")
|
||||
API_HOST_NAME = getenv("API_HOST_NAME", "localhost")
|
||||
API_PUBLIC_URL = getenv("API_PUBLIC_URL", "localhost")
|
||||
API_PUBLIC_WS_URL = getenv("API_PUBLIC_WS_URL", "localhost")
|
||||
|
||||
ADMIN_BASE_URL = getenv("ADMIN_BASE_URL", "http://localhost:6012")
|
||||
HEADER_COLOUR = (
|
||||
"#81878b" # mix(govuk-colour("dark-grey"), govuk-colour("mid-grey"))
|
||||
@@ -88,6 +91,8 @@ class Config(object):
|
||||
],
|
||||
}
|
||||
|
||||
FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "false") == "true"
|
||||
|
||||
|
||||
def _s3_credentials_from_env(bucket_prefix):
|
||||
return {
|
||||
@@ -128,8 +133,9 @@ class Test(Development):
|
||||
WTF_CSRF_ENABLED = False
|
||||
ASSET_DOMAIN = "static.example.com"
|
||||
ASSET_PATH = "https://static.example.com/"
|
||||
|
||||
API_HOST_NAME = "http://you-forgot-to-mock-an-api-call-to"
|
||||
API_PUBLIC_URL = "http://you-forgot-to-mock-an-api-call-to"
|
||||
API_PUBLIC_WS_URL = "ws://you-forgot-to-mock-an-api-call-to"
|
||||
REDIS_URL = "redis://you-forgot-to-mock-a-redis-call-to"
|
||||
LOGO_CDN_DOMAIN = "static-logos.test.com"
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
@@ -19,6 +21,8 @@ from app.main.views.sub_navigation_dictionaries import (
|
||||
)
|
||||
from app.utils.user import user_is_logged_in
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Hook to check for feature flags
|
||||
@main.before_request
|
||||
@@ -33,7 +37,7 @@ def check_feature_flags():
|
||||
@main.route("/test/feature-flags")
|
||||
def test_feature_flags():
|
||||
return jsonify(
|
||||
{"FEATURE_ABOUT_PAGE_ENABLED": current_app.config["FEATURE_ABOUT_PAGE_ENABLED"]}
|
||||
{"FEATURE_SOCKET_ENABLED": current_app.config["FEATURE_SOCKET_ENABLED"]}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
@@ -57,8 +58,12 @@ def view_job(service_id, job_id):
|
||||
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
api_public_url = os.environ.get("API_PUBLIC_URL")
|
||||
|
||||
return render_template(
|
||||
"views/jobs/job.html",
|
||||
api_public_url=api_public_url,
|
||||
FEATURE_SOCKET_ENABLED=current_app.config["FEATURE_SOCKET_ENABLED"],
|
||||
job=job,
|
||||
status=request.args.get("status", ""),
|
||||
updates_url=url_for(
|
||||
|
||||
@@ -481,6 +481,7 @@ def send_one_off_step(service_id, template_id, step_index):
|
||||
),
|
||||
template=template,
|
||||
form=form,
|
||||
current_placeholder=current_placeholder,
|
||||
skip_link=get_skip_link(step_index, template),
|
||||
back_link=back_link,
|
||||
link_to_upload=(
|
||||
@@ -560,24 +561,47 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
|
||||
)
|
||||
|
||||
if request.args.get("from_test"):
|
||||
# TODO: may not be required after letters code removed
|
||||
back_link = url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
)
|
||||
back_link_from_preview = url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
)
|
||||
back_link = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
),
|
||||
"text": "Back to message personalization",
|
||||
},
|
||||
"html": "Back to message personalization",
|
||||
}
|
||||
back_link_from_preview = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_one_off", service_id=service_id, template_id=template.id
|
||||
),
|
||||
"text": "Back to message personalization",
|
||||
},
|
||||
"html": "Back to message personalization",
|
||||
}
|
||||
choose_time_form = None
|
||||
else:
|
||||
back_link = url_for(
|
||||
"main.send_messages", service_id=service_id, template_id=template.id
|
||||
)
|
||||
back_link_from_preview = url_for(
|
||||
"main.check_messages",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
upload_id=upload_id,
|
||||
)
|
||||
back_link = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_messages", service_id=service_id, template_id=template.id
|
||||
),
|
||||
"text": "Back to upload a file",
|
||||
},
|
||||
"html": "Back to upload a file",
|
||||
}
|
||||
back_link_from_preview = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.check_messages",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
upload_id=upload_id,
|
||||
),
|
||||
"text": "Back to check messages",
|
||||
},
|
||||
"html": "Back to check messages",
|
||||
}
|
||||
choose_time_form = ChooseTimeForm()
|
||||
|
||||
if preview_row < 2:
|
||||
@@ -755,31 +779,62 @@ def get_back_link(
|
||||
preview=False,
|
||||
):
|
||||
if preview:
|
||||
return url_for(
|
||||
"main.check_notification",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
)
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.check_notification",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
),
|
||||
"text": "Back to select delivery time",
|
||||
},
|
||||
"html": "Back to select delivery time",
|
||||
}
|
||||
|
||||
if step_index == 0:
|
||||
if should_skip_template_page(template._template):
|
||||
return url_for(
|
||||
".choose_template",
|
||||
service_id=service_id,
|
||||
)
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
".choose_template",
|
||||
service_id=service_id,
|
||||
),
|
||||
"text": "Back to all templates",
|
||||
},
|
||||
"html": "Back to all templates",
|
||||
}
|
||||
else:
|
||||
return url_for(
|
||||
".view_template",
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
".view_template",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
),
|
||||
"text": "Back to confirm your template",
|
||||
},
|
||||
"html": "Back to confirm your template",
|
||||
}
|
||||
|
||||
# fallback for other steps
|
||||
back_to_text = (
|
||||
"Back to select recipients"
|
||||
if step_index == 1
|
||||
else "Back to message personalization"
|
||||
)
|
||||
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.send_one_off_step",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
)
|
||||
|
||||
return url_for(
|
||||
"main.send_one_off_step",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
step_index=step_index - 1,
|
||||
)
|
||||
step_index=step_index - 1,
|
||||
),
|
||||
"text": back_to_text,
|
||||
},
|
||||
"html": back_to_text,
|
||||
}
|
||||
|
||||
|
||||
def get_skip_link(step_index, template):
|
||||
@@ -879,7 +934,7 @@ def _check_notification(service_id, template_id, exception=None, **kwargs):
|
||||
if (not session.get("recipient")) or not all_placeholders_in_session(
|
||||
template.placeholders
|
||||
):
|
||||
raise PermanentRedirect(back_link)
|
||||
raise PermanentRedirect(back_link["href"]["url"])
|
||||
|
||||
template.values = get_recipient_and_placeholders_from_session(
|
||||
template.template_type
|
||||
|
||||
@@ -139,14 +139,28 @@ def tour_step(service_id, template_id, step_index):
|
||||
|
||||
def _get_tour_step_back_link(service_id, template_id, step_index):
|
||||
if step_index == 1:
|
||||
return url_for(".begin_tour", service_id=service_id, template_id=template_id)
|
||||
|
||||
return url_for(
|
||||
".tour_step",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=step_index - 1,
|
||||
)
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.begin_tour", service_id=service_id, template_id=template_id
|
||||
),
|
||||
"text": "Back to tour start",
|
||||
},
|
||||
"html": "Back to tour start",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.tour_step",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
step_index=step_index - 1,
|
||||
),
|
||||
"text": "Back to previous step",
|
||||
},
|
||||
"html": "Back to previous step",
|
||||
}
|
||||
|
||||
|
||||
@main.route(
|
||||
@@ -183,12 +197,18 @@ def check_tour_notification(service_id, template_id):
|
||||
)
|
||||
)
|
||||
|
||||
back_link = url_for(
|
||||
".tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=len(placeholders),
|
||||
)
|
||||
back_link = {
|
||||
"href": {
|
||||
"url": url_for(
|
||||
"main.tour_step",
|
||||
service_id=current_service.id,
|
||||
template_id=template_id,
|
||||
step_index=len(placeholders),
|
||||
),
|
||||
"text": "Back to previous step",
|
||||
},
|
||||
"html": "Back to previous step",
|
||||
}
|
||||
|
||||
template.values = get_recipient_and_placeholders_from_session(
|
||||
template.template_type
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from app.extensions import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
|
||||
@@ -81,7 +83,13 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
"""
|
||||
Retrieve a list of live services data with contact names and notification counts.
|
||||
"""
|
||||
return self.get("/service/live-services-data", params=params_dict)
|
||||
try:
|
||||
return self.get("/service/live-services-data", params=params_dict)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
f"Error getting live services data {str(e)}", exc_info=True
|
||||
)
|
||||
raise e
|
||||
|
||||
def get_active_services(self, params_dict=None):
|
||||
"""
|
||||
@@ -136,19 +144,29 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
|
||||
@cache.delete("live-service-and-organization-counts")
|
||||
def update_status(self, service_id, live):
|
||||
return self.update_service(
|
||||
service_id,
|
||||
message_limit=250000 if live else 50,
|
||||
restricted=(not live),
|
||||
go_live_at=str(datetime.utcnow()) if live else None,
|
||||
)
|
||||
try:
|
||||
return self.update_service(
|
||||
service_id,
|
||||
message_limit=250000 if live else 50,
|
||||
restricted=(not live),
|
||||
go_live_at=str(datetime.utcnow()) if live else None,
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error updating status {str(e)}", exc_info=True)
|
||||
raise e
|
||||
|
||||
@cache.delete("live-service-and-organization-counts")
|
||||
def update_count_as_live(self, service_id, count_as_live):
|
||||
return self.update_service(
|
||||
service_id,
|
||||
count_as_live=count_as_live,
|
||||
)
|
||||
try:
|
||||
return self.update_service(
|
||||
service_id,
|
||||
count_as_live=count_as_live,
|
||||
)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
f"Error updating count as live {str(e)}", exc_info=True
|
||||
)
|
||||
raise e
|
||||
|
||||
# This method is not cached because it calls through to one which is
|
||||
def update_service_with_properties(self, service_id, properties):
|
||||
|
||||
@@ -31,7 +31,7 @@ def get_csv_upload(service_id, upload_id):
|
||||
def remove_blank_lines(filedata):
|
||||
# sometimes people upload files with hundreds of blank lines at the end
|
||||
data = filedata["data"]
|
||||
cleaned_data = "\n".join(line for line in data.splitlines() if line.strip())
|
||||
cleaned_data = "\r\n".join(line for line in data.splitlines() if line.strip())
|
||||
filedata["data"] = cleaned_data
|
||||
return filedata
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
{% include "components/head.html" %}
|
||||
</head>
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
<div id="upload-status-live"
|
||||
class="usa-sr-only"
|
||||
aria-live="assertive"
|
||||
tabindex="-1"
|
||||
role="status">
|
||||
</div>
|
||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||
{% block bodyStart %}
|
||||
{% block extra_javascripts_before_body %}
|
||||
@@ -29,35 +35,11 @@
|
||||
|
||||
{% block header %}
|
||||
{% include 'components/usa_banner.html' %}
|
||||
{% if current_user.is_authenticated or current_service or current_user.platform_admin %}
|
||||
<section class="usa-site-alert usa-site-alert--info" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
GSA will no longer offer the Notify.gov service after June 8th, 2025. Visit
|
||||
<a class="usa-link" href="/notify-service-ending">Notify.gov Service Ending</a> for more information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-heading" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
Notify.gov is no longer accepting new partners.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% include 'components/header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-container minh-mobile">
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -132,7 +114,7 @@
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timeLeft">
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="sessionTimerHeading">
|
||||
<h2 class="usa-modal__heading font-body-lg" id="sessionTimerHeading">
|
||||
Your session will end soon.
|
||||
<span class="usa-sr-only">Please choose to extend your session or sign out. Your session will expire in 5 minutes or less.</span>
|
||||
</h2>
|
||||
@@ -149,7 +131,7 @@
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button type="button" class="usa-button usa-button--unstyled padding-105 text-center" id="logOutTimer"
|
||||
<button type="button" class="text-center usa-button usa-button--unstyled padding-105" id="logOutTimer"
|
||||
data-close-modal>
|
||||
Sign out
|
||||
</button>
|
||||
@@ -168,7 +150,7 @@
|
||||
{% block extra_javascripts %}
|
||||
{% endblock %}
|
||||
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
<script src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None, thing=None) %}
|
||||
<div
|
||||
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
|
||||
{% if type == 'dangerous' %}
|
||||
role='group'
|
||||
tabindex='-1'
|
||||
{% endif %}
|
||||
{% if id %}
|
||||
id={{ id }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{% macro usaBackLink(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{%- include "./template.njk" with context %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<nav class="usa-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="{{ params.href.url or params.href or '#' }}" class="usa-link usa-back-link display-inline-flex {{ params.classes or '' }}"
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {% endfor %}>{{ params.html | safe or params.href.text or 'Back' }}</a>
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {% endfor %}>
|
||||
{{- params.html | safe or params.href.text or 'Back' -}}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{# Determine type of element to use, if not explicitly set -#}
|
||||
|
||||
{% if params.element %}
|
||||
{% set element = params.element | lower %}
|
||||
{% else %}
|
||||
@@ -10,26 +9,35 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{#- Define common attributes that we can use across all element types #}
|
||||
|
||||
{# Define common attributes to use across all element types -#}
|
||||
{%- set commonAttributes %} class="usa-button{% if params.classes %} {{ params.classes }}{% endif %}{% if params.disabled %} usa-button--disabled{% endif %}"{% for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}{% endset %}
|
||||
|
||||
{#- Define common attributes we can use for both button and input types #}
|
||||
|
||||
{# Define attributes for button/input -#}
|
||||
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name | trim }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
||||
|
||||
{#- Actually create a button... or a link! #}
|
||||
|
||||
{%- if element == 'a' %}
|
||||
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{- commonAttributes | safe }}>
|
||||
{# Auto-append .dot-anim span for Send/Schedule buttons -#}
|
||||
{%- set isSendOrSchedule = params.name and (params.name | lower == 'send' or params.name | lower == 'schedule') -%}
|
||||
{%- set textContent %}
|
||||
{{ params.html | safe if params.html else params.text }}
|
||||
</a>
|
||||
{%- if isSendOrSchedule -%}<span class="dot-anim" aria-hidden="true"></span>{%- endif -%}
|
||||
{%- endset %}
|
||||
|
||||
{%- elseif element == 'button' %}
|
||||
<button {%- if params.value %} value="{{ params.value }}"{% endif %} {{- buttonAttributes | safe }} {{- commonAttributes | safe }} {% if params.disabled %}disabled{% endif %}>
|
||||
{{ params.html | safe if params.html else params.text }}
|
||||
</button>
|
||||
{# Render the appropriate element -#}
|
||||
{% if element == 'a' %}
|
||||
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{ commonAttributes | safe }}>
|
||||
{{ textContent | safe }}
|
||||
</a>
|
||||
|
||||
{%- elseif element == 'input' %}
|
||||
<input value="{{ params.text }}" {{- buttonAttributes | safe }} {{- commonAttributes | safe }}>
|
||||
{%- endif %}
|
||||
{% elseif element == 'button' %}
|
||||
<button
|
||||
{% if params.value %} value="{{ params.value }}"{% endif %}
|
||||
{{ buttonAttributes | safe }}
|
||||
{{ commonAttributes | safe }}
|
||||
{% if params.disabled %}disabled{% endif %}
|
||||
>
|
||||
{{ textContent | safe }}
|
||||
</button>
|
||||
|
||||
{% elseif element == 'input' %}
|
||||
<input value="{{ params.text }}" {{ buttonAttributes | safe }} {{ commonAttributes | safe }}>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
{% from "../hint/macro.njk" import usaHint %}
|
||||
{% from "../label/macro.njk" import usaLabel %}
|
||||
|
||||
{#- a record of other elements that we need to associate with the input using
|
||||
aria-describedby – for example hints or error messages -#}
|
||||
{# Generate a consistent input ID #}
|
||||
{% set inputId = params.id if params.id else params.label.text | default('unknown') | slugify %}
|
||||
{% set describedBy = params.describedBy if params.describedBy else "" %}
|
||||
|
||||
<div class="usa-form-group {%- if params.errorMessage %} usa-form-group--error{% endif %} {%- if params.formGroup.classes %} {{ params.formGroup.classes }}{% endif %}">
|
||||
{{ usaLabel({
|
||||
html: params.label.html,
|
||||
@@ -12,38 +13,43 @@
|
||||
classes: params.label.classes,
|
||||
isPageHeading: params.label.isPageHeading,
|
||||
attributes: params.label.attributes,
|
||||
for: params.text
|
||||
for: inputId
|
||||
}) | indent(2) | trim }}
|
||||
{% if params.hint %}
|
||||
{% set hintId = params.id + '-hint' %}
|
||||
{% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
|
||||
{{ usaHint({
|
||||
id: hintId,
|
||||
classes: params.hint.classes,
|
||||
attributes: params.hint.attributes,
|
||||
html: params.hint.html,
|
||||
text: params.hint.text
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
{% if params.errorMessage %}
|
||||
{% set errorId = params.label.text + '-error' %}
|
||||
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
||||
{{ usaErrorMessage({
|
||||
id: errorId,
|
||||
classes: params.errorMessage.classes,
|
||||
attributes: params.errorMessage.attributes,
|
||||
html: params.errorMessage.html,
|
||||
text: params.errorMessage.text,
|
||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
|
||||
{% if params.hint %}
|
||||
{% set hintId = inputId + '-hint' %}
|
||||
{% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
|
||||
{{ usaHint({
|
||||
id: hintId,
|
||||
classes: params.hint.classes,
|
||||
attributes: params.hint.attributes,
|
||||
html: params.hint.html,
|
||||
text: params.hint.text
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
|
||||
{% if params.errorMessage %}
|
||||
{% set errorId = inputId + '-error' %}
|
||||
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
||||
{{ usaErrorMessage({
|
||||
id: errorId,
|
||||
classes: params.errorMessage.classes,
|
||||
attributes: params.errorMessage.attributes,
|
||||
html: params.errorMessage.html,
|
||||
text: params.errorMessage.text,
|
||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
|
||||
<input
|
||||
class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}"
|
||||
id="{{ params.label.text | default('unknown') | slugify }}"
|
||||
class="usa-input{%- if params.classes %} {{ params.classes }}{% endif %}{%- if params.errorMessage %} usa-input--error{% endif %}"
|
||||
id="{{ inputId }}"
|
||||
name="{{ params.name }}"
|
||||
type="{{ params.type | default('text') }}"
|
||||
{%- if params.value %} value="{{ params.value }}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy | trim }}"{% endif %}
|
||||
{# Uncomment below if you want to use aria-labelledby too
|
||||
aria-labelledby="{{ inputId }}-label" #}
|
||||
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete }}"{% endif %}
|
||||
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}
|
||||
|
||||
@@ -27,17 +27,21 @@
|
||||
</label>
|
||||
{{ field(**{
|
||||
'class': 'file-upload-field',
|
||||
'id': field.name,
|
||||
'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e
|
||||
}) }}
|
||||
<label class="file-upload-button usa-button" for="{{ field.name }}">
|
||||
{{ button_text }}
|
||||
</label>
|
||||
<button type="button"
|
||||
class="usa-button file-upload-button"
|
||||
data-module="upload-trigger"
|
||||
data-file-input-id="{{ field.name }}">
|
||||
{{ button_text }}
|
||||
</button>
|
||||
{% if alternate_link and alternate_link_text %}
|
||||
<span class="file-upload-alternate-link">
|
||||
or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<label class="file-upload-filename" for="{{ field.name }}"></label>
|
||||
<span class="file-upload-filename" aria-live="polite"></span>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }}
|
||||
</form>
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
{% set is_about_page = request.path.startswith('/about') %}
|
||||
{% set is_contact_page = request.path.startswith('/contact') %}
|
||||
{% set is_information_section = is_about_page or is_contact_page %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": request.path.startswith('/accounts')},
|
||||
{"href": url_for('main.get_started'), "text": "Using Notify", "active": request.path.startswith('/using-notify')},
|
||||
{"href": url_for('main.support'), "text": "Contact us", "active": header_navigation.is_selected('support')}
|
||||
] %}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": request.path.startswith('/accounts')},
|
||||
{"href": url_for('main.get_started'), "text": "Using Notify", "active": request.path.startswith('/using-notify')},
|
||||
{"href": url_for('main.support'), "text": "Contact us", "active": header_navigation.is_selected('support')}
|
||||
] %}
|
||||
|
||||
{% if current_user.platform_admin %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin')}] %}
|
||||
{% else %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile')}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_service %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active": secondary_navigation.is_selected('settings')},
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.platform_admin %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin')}] %}
|
||||
{% else %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile')}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_service %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active": secondary_navigation.is_selected('settings')},
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% set navigation = [
|
||||
{"href": url_for('main.about_notify'), "text": "About Notify", "active": is_about_page},
|
||||
{"href": url_for('main.contact'), "text": "Contact us", "active": is_contact_page}
|
||||
] %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
@@ -59,16 +43,7 @@
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not current_user.is_authenticated and is_information_section%}
|
||||
<div class="usa-nav__login">
|
||||
<a class="usa-button usa-button login-button login-button--primary margin-right-2"
|
||||
href="{{ initial_signin_url }}">Sign
|
||||
in with <img src="{{ asset_url('images/logo-login.svg') }}" alt="Login.gov logo">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
@@ -103,7 +78,6 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
show=False,
|
||||
form=None,
|
||||
label=None,
|
||||
autofocus=False
|
||||
autofocus=True
|
||||
) %}
|
||||
{%- set search_label = label or form.search.label.text %}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
} %}
|
||||
|
||||
{% if autofocus %}
|
||||
{% set x=param_extensions.__setitem__("attributes", {"data-module": "autofocus"}) %}
|
||||
{% set _ = param_extensions.__setitem__("attributes", {"autofocus": "autofocus"}) %}
|
||||
{% endif %}
|
||||
|
||||
{% if show %}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
highlight_placeholders=False,
|
||||
autofocus=False,
|
||||
autosize=False,
|
||||
colour_preview=False,
|
||||
help_link=None,
|
||||
help_link_text=None,
|
||||
width='2-3',
|
||||
@@ -13,58 +12,82 @@
|
||||
safe_error_message=False,
|
||||
rows=8,
|
||||
extra_form_group_classes='',
|
||||
placeholder=''
|
||||
placeholder='',
|
||||
input_id=None
|
||||
) %}
|
||||
{% set field_id = kwargs.id if kwargs.id is defined else field.id %}
|
||||
{% set described_by = '' %}
|
||||
{% if hint %}{% set described_by = described_by + field_id ~ '-hint' %}{% endif %}
|
||||
{% if field.errors %}
|
||||
{% if described_by %}{% set described_by = described_by + ' ' %}{% endif %}
|
||||
{% set described_by = described_by + field_id ~ '-error' %}
|
||||
{% endif %}
|
||||
|
||||
<div
|
||||
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
|
||||
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
|
||||
>
|
||||
<label class="usa-label" for="{{ field.name }}">
|
||||
{% if label %}
|
||||
{{ label }}
|
||||
{% else %}
|
||||
{{ field.label.text }}
|
||||
{% endif %}
|
||||
<label class="usa-label" for="{{ field_id }}">
|
||||
{{ label or field.label.text }}
|
||||
</label>
|
||||
|
||||
{% if hint %}
|
||||
<div class="usa-hint">
|
||||
<div class="usa-hint" id="{{ field_id }}-hint">
|
||||
{{ hint }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<span id="{{ field.name}}-error" class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
|
||||
<span id="{{ field_id }}-error" class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
|
||||
<span class="usa-sr-only">Error:</span>
|
||||
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
|
||||
{% if not safe_error_message %}
|
||||
{{ field.errors[0] }}
|
||||
{% else %}
|
||||
{{ field.errors[0] | safe }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{%
|
||||
if highlight_placeholders or autosize
|
||||
%}
|
||||
|
||||
{% if highlight_placeholders or autosize %}
|
||||
{% set field_class = 'usa-textarea form-control-{} textbox-highlight-textbox'.format(width) %}
|
||||
{% else %}
|
||||
{% set field_class = 'form-control-{} {}'.format(width, 'textbox-right-aligned' if suffix else '') %}
|
||||
{% set field_class = 'usa-input' %}
|
||||
{% endif %}
|
||||
{%
|
||||
set field_class = 'form-control ' + field_class + (
|
||||
' form-control-error' if field.errors else ''
|
||||
)
|
||||
%}
|
||||
{{ field(
|
||||
class=field_class,
|
||||
data_module='enhanced-textbox' if highlight_placeholders or autosize else '',
|
||||
data_highlight_placeholders='true' if highlight_placeholders else 'false',
|
||||
rows=rows|string,
|
||||
placeholder=placeholder,
|
||||
aria_describedby=field.name+"-error",
|
||||
required='required' if required else None,
|
||||
**kwargs
|
||||
) }}
|
||||
{% set field_class = 'form-control ' ~ field_class ~ (' form-control-error' if field.errors else '') %}
|
||||
|
||||
{% if autosize or highlight_placeholders %}
|
||||
<textarea
|
||||
id="{{ field_id }}"
|
||||
name="{{ field.name }}"
|
||||
class="{{ field_class }}"
|
||||
rows="{{ rows }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-describedby="{{ described_by.strip() }}"
|
||||
{% if field.flags.required %}required{% endif %}
|
||||
data-module="enhanced-textbox"
|
||||
data-highlight-placeholders="true"
|
||||
>{{ field._value() }}</textarea>
|
||||
{% else %}
|
||||
{% set input_type = 'tel' if 'phone' in field.label.text else 'text' %}
|
||||
<input
|
||||
id="{{ field_id }}"
|
||||
name="{{ field.name }}"
|
||||
type="{{ input_type }}"
|
||||
class="{{ field_class }}"
|
||||
value="{{ field._value() }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-describedby="{{ described_by.strip() }}"
|
||||
{% if field.flags.required %}required{% endif %}
|
||||
/>
|
||||
{% endif %}
|
||||
|
||||
{% if suffix %}
|
||||
<span>{{ suffix }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if help_link and help_link_text %}
|
||||
<p class="textbox-help-link">
|
||||
<a class="usa-link" href='{{ help_link }}'>{{ help_link_text }}</a>
|
||||
<a class="usa-link" href="{{ help_link }}">{{ help_link_text }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
17
app/templates/error/technical_difficulties.html
Normal file
17
app/templates/error/technical_difficulties.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<section class="grid-container usa-section margin-top-5 margin-bottom-5">
|
||||
<div class="grid-row flex-justify-center">
|
||||
<div class="desktop:grid-col-3 tablet:grid-col-4 mobile:grid-col-12 flex-align-center">
|
||||
<img
|
||||
class="width-tablet tablet:width-full"
|
||||
src="{{ asset_url('images/api-error.svg') }}"
|
||||
alt="Mobile phone showing error screen"/>
|
||||
</div>
|
||||
<div class="desktop:grid-col-6 tablet:grid-col-12 padding-left-0 desktop:padding-left-5">
|
||||
<h1 class="font-body-3xl line-height-sans-2 text-bold">
|
||||
There's currently a technical issue.
|
||||
</h1>
|
||||
<p class="font-body-lg line-height-sans-2">Thank you for your patience while we work on it. Notify will be back soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -57,7 +57,7 @@
|
||||
{{notification.key_name}}
|
||||
</span>
|
||||
<span class="grid-col-6 api-notifications-item__meta-time">
|
||||
<time class="timeago" datetime="{{ notification.created_at }}">
|
||||
<time datetime="{{ notification.created_at }}">
|
||||
{{ notification.created_at|format_delta }}
|
||||
</time>
|
||||
</span>
|
||||
|
||||
@@ -20,7 +20,11 @@ Error
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
|
||||
|
||||
<div class="">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-error" class="usa-sr-only">File upload failed</span>
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
|
||||
{% if recipients.too_many_rows %}
|
||||
@@ -150,7 +154,7 @@ Error
|
||||
) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="#content" class="usa-link back-to-top-link">Back to top</a>
|
||||
<a href="#content" class="usa-link back-to-top-link display-block margin-top-2">Back to top</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -21,11 +21,13 @@
|
||||
|
||||
{{ template|string }}
|
||||
<div class="bottom-gutter-3-2">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-success" class="usa-sr-only">File uploaded successfully</span>
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{% if choose_time_form %}
|
||||
{{ choose_time_form.scheduled_for(param_extensions={
|
||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||
'formGroup': {'classes': ''},
|
||||
'attributes': {
|
||||
'data-module': 'radio-select',
|
||||
'data-categories': choose_time_form.scheduled_for.categories|join(','),
|
||||
@@ -37,7 +39,7 @@
|
||||
{% set button_text %}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{{ usaButton({ "text": button_text, "classes": "margin-top-4" }) }}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link_from_preview }) }}
|
||||
{{ usaBackLink(back_link_from_preview) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-failed" class="usa-sr-only">File upload failed</span>
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% if row_errors|length == 1 %}
|
||||
<div class="usa-alert usa-alert--error" role="alert">
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({
|
||||
"href": url_for('main.choose_template', service_id=current_service.id, template_folder_id=template_folder_id) if template_folder_id else url_for('main.choose_template', service_id=current_service.id)
|
||||
"href": url_for('main.choose_template', service_id=current_service.id, template_folder_id=template_folder_id) if template_folder_id else url_for('main.choose_template', service_id=current_service.id),
|
||||
"html": "Back to all templates"
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -14,19 +14,22 @@
|
||||
</h1>
|
||||
|
||||
|
||||
{% call form_wrapper(
|
||||
action=url_for('.find_services_by_name'),
|
||||
class='usa-search margin-bottom-4'
|
||||
) %}
|
||||
{{ form.search(param_extensions={
|
||||
"label": {"text": "Find services by name, partial name, or service ID"}
|
||||
}) }}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{{ usaButton({
|
||||
"text": "Search",
|
||||
"classes": "usa-button search-form__button"
|
||||
}) }}
|
||||
{% endcall %}
|
||||
{% call form_wrapper(
|
||||
action=url_for('.find_services_by_name'),
|
||||
class='usa-search margin-bottom-4'
|
||||
) %}
|
||||
<div class="display-flex flex-justify-start flex-align-end gap-1 width-full">
|
||||
{{ form.search(param_extensions={
|
||||
"label": {"text": "Find services by name, partial name, or service ID"},
|
||||
"classes": "width-full"
|
||||
}) }}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{{ usaButton({
|
||||
"text": "Search",
|
||||
"classes": "usa-button search-form__button"
|
||||
}) }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% call form_wrapper(id='search-form' ) %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -15,17 +15,20 @@
|
||||
|
||||
|
||||
{% call form_wrapper(
|
||||
action=url_for('.find_users_by_email'),
|
||||
class='usa-search margin-bottom-4'
|
||||
action=url_for('.find_users_by_email'),
|
||||
class='usa-search margin-bottom-4'
|
||||
) %}
|
||||
<div class="display-flex flex-align-end gap-1 width-full">
|
||||
{{ form.search(param_extensions={
|
||||
"label": {"text": "Find users by email, or by partial email"}
|
||||
"label": {"text": "Find users by email, or by partial email"},
|
||||
"classes": "width-full"
|
||||
}) }}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{{ usaButton({
|
||||
"text": "Search",
|
||||
"classes": "search-form__button"
|
||||
"classes": "usa-button search-form__button"
|
||||
}) }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% call form_wrapper(id='search-form' ) %}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<p class="hint">This person has never logged in</p>
|
||||
{% else %}
|
||||
<p>Last logged in
|
||||
<time class="timeago" datetime="{{ user.logged_in_at }}">
|
||||
<time datetime="{{ user.logged_in_at }}">
|
||||
{{ user.logged_in_at|format_delta }}
|
||||
</time>
|
||||
</p>
|
||||
|
||||
@@ -11,10 +11,18 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
{% if not job.finished_processing %}
|
||||
<p class="max-width-full">This page refreshes automatically to show the latest message activity delivery rates, details, and reports.<br>You can watch it in progress or check back later.</p>
|
||||
{% endif %}
|
||||
<div data-job-id="{{ job.id }}" data-feature="{{FEATURE_SOCKET_ENABLED | lower}}" data-host="{{ api_public_url }}">
|
||||
{% if not job.finished_processing %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="status"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="status"
|
||||
data-form=""
|
||||
>
|
||||
@@ -25,8 +33,12 @@
|
||||
{% endif %}
|
||||
{% if not finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="counts"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="counts"
|
||||
data-form=""
|
||||
>
|
||||
@@ -41,8 +53,12 @@
|
||||
</p>
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="notifications"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="notifications"
|
||||
data-form=""
|
||||
>
|
||||
@@ -51,7 +67,6 @@
|
||||
{% if not job.processing_finished %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -30,78 +30,105 @@
|
||||
</div>
|
||||
|
||||
{% if show_search_box %}
|
||||
<div data-module="autofocus">
|
||||
<div>
|
||||
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="user-list">
|
||||
<div class="js-live-search-no-results" style="display: none;">
|
||||
<p class="usa-body margin-0">No results found</p>
|
||||
</div>
|
||||
{% for user in users %}
|
||||
{% if user.status != 'cancelled' %}
|
||||
<div class="user-list-item">
|
||||
<h2 class="user-list-item-heading font-body-lg margin-top-0" title="{{ user.email_address }}">
|
||||
<div class="user-list-item width-full">
|
||||
<h2 class="user-list-item-heading font-body-lg margin-y-0" title="{{ user.email_address }}">
|
||||
{%- if user.name -%}
|
||||
<span class="heading-small live-search-relevant">{{ user.name }}</span>
|
||||
<span class="heading-small live-search-relevant">{{ user.name }}</span>
|
||||
{%- else -%}
|
||||
<span class="heading-small live-search-relevant">{{ user.email_address }}</span>
|
||||
{%- endif -%}
|
||||
</h2>
|
||||
<p class="margin-top-0">
|
||||
{%- if user.status == 'pending' -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(invited)</span>
|
||||
{%- elif user.status == 'cancelled' -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(cancelled invite)</span>
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
<span class="hint">(invited)</span>
|
||||
{%- elif user.status == 'expired' -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(expired invite)</span>
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
<span class="hint">(expired invite)</span>
|
||||
{%- elif user.id == current_user.id -%}
|
||||
<span class="live-search-relevant"></span><span class="hint">(you)</span>
|
||||
{% else %}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
<span class="hint">(you)</span>
|
||||
{%- else -%}
|
||||
<span class="live-search-relevant">{{ user.email_address }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<h3 class="margin-bottom-0">Permissions</h3>
|
||||
<ul class="tick-cross-list-permissions">
|
||||
{% for permission, label in permissions %}
|
||||
{{ tick_cross(
|
||||
user.has_permission_for_service(current_service.id, permission),
|
||||
label
|
||||
) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{# only show if the service has folders #}
|
||||
{% if current_service.all_template_folders %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
{% set folder_count = user.template_folders_for_service(current_service) | length %}
|
||||
{% if folder_count == 0 %}
|
||||
Cannot see any folders
|
||||
{% elif folder_count != current_service.all_template_folders | length %}
|
||||
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
|
||||
{% else %}
|
||||
Can see all folders
|
||||
{% endif%}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if current_service.has_permission('email_auth') %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
Signs in with
|
||||
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if current_service.has_permission('email_auth') %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
Signs in with
|
||||
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if user.status == 'pending' or user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id)}}">Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
|
||||
</p>
|
||||
|
||||
<div class="usa-accordion usa-accordion--bordered margin-bottom-2">
|
||||
<h3 class="margin-bottom-0 usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button"
|
||||
aria-expanded="false"
|
||||
aria-controls="permissions_{{user.id}}"
|
||||
>
|
||||
Permissions
|
||||
</button>
|
||||
</h3>
|
||||
<div id="permissions_{{user.id}}" class="usa-accordion__content usa-prose" hidden>
|
||||
<ul class="tick-cross-list-permissions">
|
||||
{% for permission, label in permissions %}
|
||||
{{ tick_cross(
|
||||
user.has_permission_for_service(current_service.id, permission),
|
||||
label
|
||||
) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if current_service.all_template_folders %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
{% set folder_count = user.template_folders_for_service(current_service) | length %}
|
||||
{% if folder_count == 0 %}
|
||||
Cannot see any folders
|
||||
{% elif folder_count != current_service.all_template_folders | length %}
|
||||
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
|
||||
{% else %}
|
||||
Can see all folders
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id)}}">Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
|
||||
{% elif user.is_editable_by(current_user) %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id)}}">Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span></a>
|
||||
|
||||
{% if current_service.has_permission('email_auth') %}
|
||||
<p class="usa-body tick-cross-list-hint">
|
||||
Signs in with
|
||||
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if user.status == 'pending' or user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id) }}">
|
||||
Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id) }}">
|
||||
Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span>
|
||||
</a>
|
||||
{% elif user.is_editable_by(current_user) %}
|
||||
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id) }}">
|
||||
Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -55,7 +55,7 @@
|
||||
{% if not error %}
|
||||
{% if choose_time_form %}
|
||||
{{ choose_time_form.scheduled_for(param_extensions={
|
||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||
'formGroup': {'classes': ''},
|
||||
'attributes': {
|
||||
'data-module': 'radio-select',
|
||||
'data-categories': choose_time_form.scheduled_for.categories|join(','),
|
||||
@@ -66,8 +66,8 @@
|
||||
{% set button_text %}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{% endif %}
|
||||
{{ usaButton({ "text": button_text, "classes": "margin-top-2" }) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
|
||||
{% block backLink %}
|
||||
{% if help %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% else %}
|
||||
{{ usaBackLink({ "href": back_link_from_preview }) }}
|
||||
{{ usaBackLink(back_link_from_preview) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
help='3' if help else 0
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<h3>Does everything look good?</h3>
|
||||
<h3 class="margin-bottom-">Does everything look good?</h3>
|
||||
{% if not error %}
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
@@ -84,9 +84,54 @@
|
||||
{{ usaButton({
|
||||
"text": button_text,
|
||||
"name": button_text
|
||||
}) }}
|
||||
}) }}
|
||||
{{ usaButton({
|
||||
"text": "Cancel",
|
||||
"name": "Cancel",
|
||||
"classes": "usa-button--secondary",
|
||||
"type": "button",
|
||||
"attributes": {
|
||||
"data-open-modal": "cancelModal"
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="usa-modal"
|
||||
data-module="usa-modal"
|
||||
id="cancelModal"
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cancelModalHeading"
|
||||
aria-describedby="cancelModalDesc"
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading font-body-lg" id="cancelModalHeading">Are you sure you want to cancel this message?</h2>
|
||||
<p id="cancelModalDesc">Your template is saved, but your message and recipient details will not be. Canceling will bring you back to the template selection page.</p>
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<a href="{{ url_for('main.choose_template', service_id=current_service.id) }}" class="usa-button">Yes, cancel
|
||||
<span class="usa-sr-only">and return to the template selection page</span></a>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button class="usa-button usa-button--unstyled padding-105 text-center" data-close-modal type="button">
|
||||
No, go back <span class="usa-sr-only">to message send</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="usa-button usa-modal__close" aria-label="Close this window" data-close-modal type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
</h1>
|
||||
|
||||
{% if show_search_box %}
|
||||
<div data-module="autofocus">
|
||||
<div>
|
||||
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="user-list">
|
||||
{% for user in users %}
|
||||
{% for user in users %}
|
||||
<div class="user-list">
|
||||
<div class="user-list-item">
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-9">
|
||||
@@ -60,8 +60,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
{{ usaButton({
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="grid-row bottom-gutter">
|
||||
{% for noti_type in global_stats %}
|
||||
<div class="grid-col-6">
|
||||
<span class="big-number-dark bottom-gutter-2-3">
|
||||
<span class="big-number-dark">
|
||||
<span class="big-number-number">
|
||||
{{ "{:,}".format(noti_type.black_box.number) }}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
@@ -9,7 +10,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{{ usaBackLink(back_link) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -38,10 +39,18 @@
|
||||
data_force_focus=True
|
||||
) %}
|
||||
<div class="grid-row">
|
||||
{% set extra_class = "extra-tracking" if form.placeholder_value.label.text == "phone number" else "" %}
|
||||
{% set placeholder_id = "phone number" if form.placeholder_value.label.text == "phone number" else "" %}
|
||||
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
||||
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
|
||||
{% set is_phone_number = form.placeholder_value.label.text == "phone number" %}
|
||||
{% set label_text = " phone number" if current_placeholder == "phone number" else current_placeholder %}
|
||||
{% set extra_class = "extra-tracking" if is_phone_number else "" %}
|
||||
|
||||
<div class="grid-col-12 {{ extra_class }}">
|
||||
{{ textbox(
|
||||
form.placeholder_value,
|
||||
label=label_text,
|
||||
hint=hint_text,
|
||||
extra_form_group_classes=extra_class,
|
||||
param_extensions={"id": "phone-number", "classes": ""}
|
||||
) }}
|
||||
</div>
|
||||
{% if skip_link or link_to_upload %}
|
||||
<div class="grid-col-12 margin-top-1">
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink(params) }}
|
||||
{{ usaBackLink({
|
||||
"href": url_for('main.send_one_off_step', service_id=current_service.id, template_id=template.id, step_index=0),
|
||||
"html": "Back to select recipients"
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% set phone_numbers = [
|
||||
@@ -68,13 +71,14 @@
|
||||
show_errors=False
|
||||
)}}
|
||||
</div>
|
||||
<span id="upload-status-live" class="usa-sr-only" role="status" aria-live="polite" aria-atomic="true"></span>
|
||||
|
||||
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
|
||||
|
||||
<div class="spreadsheet" data-module="fullscreen-table">
|
||||
{% call(item, row_number) list_table(
|
||||
example,
|
||||
caption="Example",
|
||||
caption="Example spreadsheet",
|
||||
caption_visible=False,
|
||||
field_headings=[''] + column_headings
|
||||
) %}
|
||||
|
||||
@@ -4,199 +4,72 @@ import usaButton %} {% block meta %}
|
||||
name="description"
|
||||
content="Notify.gov lets you send text messages to your users. Try it now if you work in federal, state, or local government."
|
||||
/>
|
||||
{% endblock %} {% block pageTitle %} Notify.gov {% endblock %} {% block main %}
|
||||
{% endblock %}
|
||||
{% block pageTitle %} Notify.gov
|
||||
{% if is_api_down %} - There's currently a technical issue {% endif %}
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
{% block beforeContent %}{% endblock %}
|
||||
|
||||
<main id="main-content" role="main">
|
||||
{% block content %}
|
||||
<section class="usa-section--dark usa-hero" aria-label="Introduction">
|
||||
<div class="grid-container padding-y-4">
|
||||
<div class="grid-row grid-gap display-flex flex-align-center">
|
||||
<div class="desktop:grid-col-8 tablet:grid-col-12">
|
||||
<h1 class="font-sans-2xl usa-hero__heading">
|
||||
Reach people where they are with government-powered text messages
|
||||
</h1>
|
||||
<p class="font-sans-lg">
|
||||
Notify.gov is a text messaging service that helps federal, state,
|
||||
local, tribal and territorial governments more effectively
|
||||
communicate with the people they serve.
|
||||
</p>
|
||||
<div class="usa-button-group margin-bottom-5 flex-align-center">
|
||||
<a
|
||||
class="usa-button usa-button login-button login-button--primary margin-right-2"
|
||||
href="{{ initial_signin_url }}"
|
||||
>Sign in with
|
||||
<img
|
||||
src="{{ asset_url('images/logo-login.svg') }}"
|
||||
alt="Login.gov logo"
|
||||
/>
|
||||
</a>
|
||||
if you are an existing partner
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<img
|
||||
src="{{ asset_url('images/product/phone-text.png') }}"
|
||||
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
|
||||
Government texting made easy
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Notify.gov is a text messaging platform built for government agencies.
|
||||
With minimal set-up and secure, personalized messaging, you can make
|
||||
one-way texting a part of your outreach program.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-sans-xl margin-top-0">Key features</h2>
|
||||
<div class="home-cards margin-top-5">
|
||||
<ul class="usa-card-group display-flex margin-bottom-4">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/internet.svg') }}"
|
||||
alt="Globe on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Web-based</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Nothing to download or install</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/fast.svg') }}"
|
||||
alt="Stopwatch with a notification speech bubble with a star inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Fast and easy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>No technical expertise required</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/status.svg') }}"
|
||||
alt="3 status messages, 2 successes and one failure"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Track message delivery</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>See which messages were received</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="usa-card-group">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/translation.svg') }}"
|
||||
alt="Speech bubbles with the letter A and the Chinese character for language"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send in recipients' preferred language
|
||||
</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Notify.gov has support for more than 30 character sets</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/security.svg') }}"
|
||||
alt="Lock with code icon inside on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Security and privacy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Limited data retention, encryption, and multi-factor
|
||||
authentication protect user data and manage risk with <br><a href="/about/security">our security efforts</a>
|
||||
{% if is_api_down %}
|
||||
{% include "error/technical_difficulties.html" %}
|
||||
{% else %}
|
||||
<section class="usa-section--dark usa-hero usa-prose" aria-label="Introduction">
|
||||
<div class="grid-container">
|
||||
<div class="grid-row">
|
||||
<div class="text-center grid-col-12 padding-y-4">
|
||||
<h1 class="font-sans-2xl usa-hero__heading margin-bottom-2">
|
||||
Sunsetting Notify.gov
|
||||
</h1>
|
||||
<p class="font-sans-lg margin-0">
|
||||
This beta product is no longer in service as of June 9, 2025.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/send.svg') }}"
|
||||
alt="Paper airplane and a notification icon with the number 1 inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send bulk, customized, one-way messages
|
||||
</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Send hundreds or thousands of individually customized messages
|
||||
with just a few clicks
|
||||
</div>
|
||||
</section>
|
||||
<section class="usa-section usa-prose" aria-label="Introduction">
|
||||
<div class="grid-container padding-y-4">
|
||||
<div class="grid-row grid-gap display-flex flex-align-center">
|
||||
<div class="desktop:grid-col-8 tablet:grid-col-12">
|
||||
<h2 class="font-body-xl">
|
||||
Text messaging services
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Though the Notify.gov service is no longer available, government programs can continue to explore text messaging services.
|
||||
</p>
|
||||
<p class="usa-body">Commercial messaging providers may support government use cases. For agencies with the resources to set up and support it, <a href="https://github.com/GSA/notifications-api" class="usa-link">the notifications service codebase</a> that runs Notify.gov is open source and can be reused.</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose grid-container padding-bottom-10">
|
||||
<div class="grid-row grid-gap-3">
|
||||
<a
|
||||
class="text-no-underline tablet:grid-col-4 mobile-lg:grid-col-12"
|
||||
href="mailto:tts-notify@gsa.gov"
|
||||
>
|
||||
<div class="contact-us-card">
|
||||
<div class="grid-row flex-align-center grid-gap-2">
|
||||
<div class="grid-col-auto">
|
||||
<div
|
||||
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<img
|
||||
src="{{ asset_url('images/contact.svg') }}"
|
||||
alt=""
|
||||
class="height-7 width-7"
|
||||
src="{{ asset_url('images/messaging-devices.svg') }}"
|
||||
alt="Illustration showing multiple devices including mobile phones and messaging interfaces representing government text messaging communication"
|
||||
width="305"
|
||||
height="304"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
<p class="margin-0">
|
||||
<strong>Contact us</strong><br />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="bg-gray-cool usa-section usa-prose" aria-label="Thank you message to partners">
|
||||
<div class="grid-container">
|
||||
<div class="grid-row flex-justify-center">
|
||||
<div class="text-center grid-col-8">
|
||||
<h2 class="font-body-xl text-primary margin-bottom-2">
|
||||
To our partners
|
||||
</h2>
|
||||
<p class="usa-body margin-0">
|
||||
Thank you to our users and partners for their support, participation, and contributions in developing Notify.gov.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<nav id="template-list" aria-label="Choose reply">
|
||||
<ul>
|
||||
{% for item in templates_and_folders %}
|
||||
<li class="template-list-item {% if item.ancestors %}template-list-item-hidden-by-default{% endif %} {% if not item.ancestors %}template-list-item-without-ancestors{% endif %}">
|
||||
<li class="template-list-item {% if item.ancestors %}template-list-item-hidden-by-default{% else %}template-list-item-without-ancestors{% endif %}">
|
||||
{% for ancestor in item.ancestors %}
|
||||
<a href="{{ url_for('.conversation_reply', service_id=current_service.id, notification_id=notification_id, from_folder=ancestor.id) }}" class="usa-link template-list-folder">
|
||||
{{ ancestor.name }}
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
{% else %}
|
||||
|
||||
<div class="grid-row flex-column">
|
||||
<h1 class="font-body-2xl line-height-sans-2 margin-bottom-1 margin-top-0">{{page_title}}</h1>
|
||||
<h1 class="font-body-2xl line-height-sans-2 margin-bottom-1 margin-top-0" id="page-title">{{page_title}}</h1>
|
||||
<div class="{% if current_user.has_permissions('manage_templates') %} grid-col-10 {% else %} grid-col-12 {% endif %}">
|
||||
<p class="margin-top-0 margin-bottom-4">
|
||||
<p class="margin-top-0 margin-bottom-4" id="page-description">
|
||||
Every message starts with a template. To send, choose or create a template.
|
||||
</p>
|
||||
{{ folder_path(
|
||||
@@ -59,6 +59,10 @@
|
||||
|
||||
{{ live_search(target_selector='#template-list .template-list-item', show=show_search_box, form=search_form) }}
|
||||
|
||||
<div class="js-live-search-no-results" style="display: none;">
|
||||
<p class="usa-body margin-top-2">No templates found</p>
|
||||
</div>
|
||||
|
||||
{% if current_user.has_permissions('manage_templates') and user_has_template_folder_permission %}
|
||||
{% call form_wrapper(
|
||||
class='sticky-scroll-area',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% from "components/folder-path.html" import copy_folder_path, page_title_folder_path %}
|
||||
{% from "components/live-search.html" import live_search %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% extends "withnav_template.html" %}
|
||||
{% set page_title = "Copy an existing template" %}
|
||||
@@ -8,6 +9,12 @@
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{% if not from_service %}
|
||||
{{ usaBackLink({ "href": url_for('.choose_template', service_id=current_service.id),
|
||||
"html": "Back to all templates" }) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="">
|
||||
@@ -25,11 +32,14 @@
|
||||
form=search_form,
|
||||
autofocus=True
|
||||
) }}
|
||||
<div class="js-live-search-no-results" style="display: none;">
|
||||
<p class="usa-body margin-top-2">No templates found</p>
|
||||
</div>
|
||||
<nav id="template-list" aria-label="Copy template list">
|
||||
<ul>
|
||||
{% for item in services_templates_and_folders %}
|
||||
|
||||
<li class="template-list-item margin-bottom-2 {% if item.ancestors %}template-list-item-hidden-by-default{% endif %} {% if not item.ancestors %}template-list-item-without-ancestors{% endif %}">
|
||||
<li class="template-list-item margin-bottom-2">
|
||||
{% if item.is_service %}
|
||||
{% elif item.is_folder %}
|
||||
{% else %}
|
||||
|
||||
22
app/utils/api_health.py
Normal file
22
app/utils/api_health.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_api_down():
|
||||
api_base_url = os.getenv("API_HOST_NAME")
|
||||
try:
|
||||
response = requests.get(api_base_url, timeout=2)
|
||||
is_down = response.status_code != 200
|
||||
if is_down:
|
||||
logger.warning(
|
||||
f"API responded with status {response.status_code} at {api_base_url}"
|
||||
)
|
||||
return is_down
|
||||
except RequestException as e:
|
||||
logger.error(f"API down when loading homepage {e}")
|
||||
return True
|
||||
@@ -198,6 +198,7 @@ def convert_report_date_to_preferred_timezone(db_date_str_in_utc):
|
||||
|
||||
def get_user_preferred_timezone():
|
||||
if current_user and hasattr(current_user, "preferred_timezone"):
|
||||
return current_user.preferred_timezone
|
||||
|
||||
tz = current_user.preferred_timezone
|
||||
if tz in pytz.all_timezones:
|
||||
return tz
|
||||
return "US/Eastern"
|
||||
|
||||
805
assets/uswds/components/usa-banner.js.mjs
Normal file
805
assets/uswds/components/usa-banner.js.mjs
Normal file
File diff suppressed because one or more lines are too long
127
assets/uswds/components/usa-banner.js.umd.js
Normal file
127
assets/uswds/components/usa-banner.js.umd.js
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 958 KiB After Width: | Height: | Size: 951 KiB |
@@ -7,3 +7,6 @@ cloud_dot_gov_route: notify-demo.app.cloud.gov
|
||||
redis_enabled: 1
|
||||
nr_agent_id: '1134302465'
|
||||
nr_app_id: '1083160688'
|
||||
FEATURE_SOCKET_ENABLED: true
|
||||
API_PUBLIC_URL: https://notify-api-demo.app.cloud.gov
|
||||
API_PUBLIC_WS_URL: wss://notify-api-demo.app.cloud.gov
|
||||
|
||||
@@ -7,3 +7,6 @@ cloud_dot_gov_route: notify.app.cloud.gov
|
||||
redis_enabled: 1
|
||||
nr_agent_id: '1050708682'
|
||||
nr_app_id: '1050708682'
|
||||
FEATURE_SOCKET_ENABLED: false
|
||||
API_PUBLIC_URL: https://notify-api-production.app.cloud.gov
|
||||
API_PUBLIC_WS_URL: wss://notify-api-production.app.cloud.gov
|
||||
|
||||
@@ -9,6 +9,7 @@ ADMIN_CLIENT_USERNAME: notify-admin
|
||||
ADMIN_CLIENT_SECRET: sandbox-notify-secret-key
|
||||
DANGEROUS_SALT: sandbox-notify-salt
|
||||
SECRET_KEY: sandbox-notify-secret-key
|
||||
FEATURE_SOCKET_ENABLED: true
|
||||
nr_agent_id: ''
|
||||
nr_app_id: ''
|
||||
NR_BROWSER_KEY: ''
|
||||
|
||||
@@ -7,3 +7,6 @@ cloud_dot_gov_route: notify-staging.app.cloud.gov
|
||||
redis_enabled: 1
|
||||
nr_agent_id: '1134291385'
|
||||
nr_app_id: '1031640326'
|
||||
FEATURE_SOCKET_ENABLED: false
|
||||
API_PUBLIC_URL: https://notify-api-staging.app.cloud.gov
|
||||
API_PUBLIC_WS_URL: wss://notify-api-staging.app.cloud.gov
|
||||
|
||||
BIN
docs/Meeting 7 Slides - T&N (1).pdf
Normal file
BIN
docs/Meeting 7 Slides - T&N (1).pdf
Normal file
Binary file not shown.
@@ -18,4 +18,4 @@ If you're working in VS Code, you can also leverage Docker for a containerized d
|
||||
|
||||
1. Go to http://localhost:6012
|
||||
|
||||
NOTE: when you change .env in the future, you'll need to rebuild the devcontainer for the change to take effect. VS Code _should_ detect the change and prompt you with a toast notification during a cached build. If not, you can find a manual rebuild in command pallette or just `docker rm` the notifications-api container.
|
||||
NOTE: when you change .env in the future, you'll need to rebuild the devcontainer for the change to take effect. VS Code _should_ detect the change and prompt you with a toast notification during a cached build. If not, you can find a manual rebuild in command palette or just `docker rm` the notifications-api container.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Using Environment Variables
|
||||
|
||||
This guide explains how to update cloud foundry envirionment variables to enable or disable feature flagging.
|
||||
This guide explains how to update cloud foundry environment variables to enable or disable feature flagging.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
| | Goals | Impact | Milestone(s) |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|
|
||||
| Engineering | Get `staging` stable and bugs fixed in order to do a prod push, finalize Login.gov implementation, complete epic to [remove phone numbers from db](https://github.com/GSA/notifications-api/issues/667), [calculate pre-send message parts](https://github.com/GSA/notifications-admin/issues/1027) to add to confirmation page, finish [E2E testing implementation](https://github.com/GSA/notifications-admin/issues/677), set up [opt-out lists by phone number](https://github.com/GSA/notifications-api/issues/312) | Install required auth, expand message send volume for partners, allow users to see how many parts a send will cost, improve app stability, partition opt-outs so that they aren't across the system | Building Notify.gov |
|
||||
| UX | [Tackle `Usage and Performance` epic](https://github.com/GSA/notifications-admin/issues/1108) as highest priority group of user stories, break out UX work into more user-stories and correlating epics, prioritize epic/story order, improve [pre-send confirmation page](https://github.com/GSA/notifications-admin/issues/815)| Improve UX | Building Notify.gov|
|
||||
| UX | [Tackle `Usage and Performance` epic](https://github.com/GSA/notifications-admin/issues/1108) as highest priority group of user stories, break out UX work into more user-stories and correlating epics, prioritize epic/story order, improve [pre-send confirmation page](https://github.com/GSA/notifications-admin/issues/815)| Improve UX | Building Notify.gov|
|
||||
| Security | Complete necessary control families | Aim to have package completed with enough time to allow for long assessment| Secure ATO |
|
||||
| Content | Get [refreshed content for `Using Notify` pages](https://github.com/GSA/notifications-admin/issues/449) in place, make content [more clear about limits](https://github.com/GSA/notifications-admin/issues/963), make content clearer by [adding `parts` where necessary](https://github.com/GSA/notifications-admin/issues/963) | Improve clarity of message parts for users, bring existing content into current state| Building Notify.gov |
|
||||
| Ops | Assist in Tech to Gov process, plan gap coverage for Tim's departure 2/9 |
|
||||
| Ops | Assist in Tech to Gov process, plan gap coverage for Tim's departure 2/9 |
|
||||
|
||||
## Sprint: Akhlut (1/3/24)
|
||||
2024 Sprints will be named after mythical creatures
|
||||
@@ -17,7 +17,7 @@
|
||||
| | Goals | Impact | Milestone(s) |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|
|
||||
| Engineering | Continue [identifying](https://github.com/GSA/notifications-api/issues/656), [fixing](https://github.com/GSA/notifications-api/issues/645), and [displaying](https://github.com/GSA/notifications-admin/issues/964) various applicable message part limits, devise ways to [create better access to reports](https://github.com/GSA/notifications-admin/issues/855), implement Login.gov on staging environment, advance epic to [remove phone numbers from db](https://github.com/GSA/notifications-api/issues/667) | Better track annual and smart-retention limits, install required auth, make reports easier to find, expand message send volume for partners | Building Notify.gov |
|
||||
| UX | Spin up weekly [redesign syncs](https://github.com/GSA/notifications-admin/issues/1061), identify necessary dashboard metrics to visualize, improve [team member section](https://github.com/GSA/notifications-admin/issues/1031)| Improve UX | Building Notify.gov|
|
||||
| UX | Spin up weekly [redesign syncs](https://github.com/GSA/notifications-admin/issues/1061), identify necessary dashboard metrics to visualize, improve [team member section](https://github.com/GSA/notifications-admin/issues/1031)| Improve UX | Building Notify.gov|
|
||||
| Security | Complete necessary control families | Aim to have package completed with enough time to allow for long assessment| Secure ATO |
|
||||
| Content | Finish making [editing content easier](https://github.com/GSA/notifications-admin/issues/976) with existing templates, finalize decision on terminology regarding [batch/campaign/job](https://github.com/GSA/notifications-admin/issues/1060), make content [more clear about limits](https://github.com/GSA/notifications-admin/issues/963), make content clearer by [adding `parts` where necessary](https://github.com/GSA/notifications-admin/issues/963) | Improve clarity of message parts for users, streamline content editing for the team| Building Notify.gov |
|
||||
| Ops | Assist in Tech to Gov process | |
|
||||
@@ -28,7 +28,7 @@ Sprint velocity was very low due to holiday OOO
|
||||
| | Goals | Impact | Milestone(s) |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|
|
||||
| Engineering | Spend time [identifying](https://github.com/GSA/notifications-api/issues/656), [fixing](https://github.com/GSA/notifications-api/issues/645), and [displaying](https://github.com/GSA/notifications-admin/issues/964) various applicable message part limits, devise ways to [create better access to reports](https://github.com/GSA/notifications-admin/issues/855), implement Login.gov on staging environment, advance epic to [remove phone numbers from db](https://github.com/GSA/notifications-api/issues/667) | Better track annual and smart-retention limits, install required auth, make reports easier to find, expand message send volume for partners | Building Notify.gov |
|
||||
| UX | [Re-think Notify IA](https://github.com/GSA/notifications-admin/issues/910), start implementing dashboard changes, improve [team member section](https://github.com/GSA/notifications-admin/issues/1031)| Improve UX | Building Notify.gov|
|
||||
| UX | [Re-think Notify IA](https://github.com/GSA/notifications-admin/issues/910), start implementing dashboard changes, improve [team member section](https://github.com/GSA/notifications-admin/issues/1031)| Improve UX | Building Notify.gov|
|
||||
| Security | Complete necessary control families | Aim to have package completed with enough time to allow for long assessment| Secure ATO |
|
||||
| Content | Make content [more clear about limits](https://github.com/GSA/notifications-admin/issues/963), make content clearer by [adding `parts` where necessary](https://github.com/GSA/notifications-admin/issues/963), test making [editing content easier](https://github.com/GSA/notifications-admin/issues/976) with existing templates | Improve clarity of message parts for users, streamline content editing for the team| Building Notify.gov |
|
||||
| Ops | Assist in Tech to Gov process | |
|
||||
@@ -39,10 +39,10 @@ Sprint velocity was very low due to holiday OOO
|
||||
| | Goals | Impact | Milestone(s) |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|
|
||||
| Engineering | Spend time [identifying](https://github.com/GSA/notifications-api/issues/656), [fixing](https://github.com/GSA/notifications-api/issues/645), and [displaying](https://github.com/GSA/notifications-admin/issues/964) various applicable message part limits, devise ways to [create better access to reports](https://github.com/GSA/notifications-admin/issues/855), implement Login.gov on staging environment | Better track annual and smart-retention limits, continuity between application orgs and agreements | Building Notify.gov |
|
||||
| UX | [Re-think Notify IA](https://github.com/GSA/notifications-admin/issues/910), start implementing dashboard changes, obtain requested user feedback on [Notify logo](https://github.com/GSA/notifications-admin/issues/859) proposal | Improve ease of use, begin process of brand creation | Obbtain approval for logo/brand|
|
||||
| UX | [Re-think Notify IA](https://github.com/GSA/notifications-admin/issues/910), start implementing dashboard changes, obtain requested user feedback on [Notify logo](https://github.com/GSA/notifications-admin/issues/859) proposal | Improve ease of use, begin process of brand creation | Obbtain approval for logo/brand|
|
||||
| Security | Complete [CP family of controls](https://github.com/GSA/us-notify-compliance/issues/42), Draft [config management plan](https://github.com/GSA/us-notify-compliance/issues/35) | Aim to have package completed with enough time to allow for long assessment| Secure ATO |
|
||||
| Content | Make content (and MOUs) [more clear about limits](https://github.com/GSA/notifications-admin/issues/963), test making [editing content easier](https://github.com/GSA/notifications-admin/issues/976) with existing templates | Improve clarity of message parts for users, streamline content editing for the team| Building Notify.gov |
|
||||
| Ops | |
|
||||
| Ops | |
|
||||
|
||||
## Sprint: Wallcreeper (11/8/23)
|
||||
(Sprint is at low capacity due to Thanksgiving week)
|
||||
@@ -50,7 +50,7 @@ Sprint velocity was very low due to holiday OOO
|
||||
| | Goals | Impact | Milestone(s) |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|
|
||||
| Engineering | Change timezones displays to [Eastern time](https://github.com/GSA/notifications-admin/issues/861), [create properties on org model](https://github.com/GSA/notifications-api/issues/331) to process agreement info | Better timezone familiarity, continuity between application orgs and agreements | Building Notify.gov |
|
||||
| UX | Begin [re-thinking Notify IA](https://github.com/GSA/notifications-admin/issues/910), create a synthesis of partner feeback thusfar, wireframe a more [user-friendly dashboard](https://github.com/GSA/notifications-admin/issues/889), send [Notify logo](https://github.com/GSA/notifications-admin/issues/859) proposal to begin approvals, add UI consideration regarding [multi-lingual messages](https://github.com/GSA/notifications-admin/issues/901) | Improve ease of use, begin process of brand creation | Obtain approval for logo/brand|
|
||||
| UX | Begin [re-thinking Notify IA](https://github.com/GSA/notifications-admin/issues/910), create a synthesis of partner feedback thusfar, wireframe a more [user-friendly dashboard](https://github.com/GSA/notifications-admin/issues/889), send [Notify logo](https://github.com/GSA/notifications-admin/issues/859) proposal to begin approvals, add UI consideration regarding [multi-lingual messages](https://github.com/GSA/notifications-admin/issues/901) | Improve ease of use, begin process of brand creation | Obtain approval for logo/brand|
|
||||
| Security | Complete pre-requisite documentation, start project planning timelines and control group deadlines | Aim to have package completed with enough time to allow for long assessment| Secure ATO |
|
||||
| Content | Finalize cloud.gov pages IAA mod for content site | Enable an easy static website for future content | Building Notify.gov |
|
||||
| Ops | Complete Beverly's onboarding with necessary access to cloud.gov, AWS, and other tools| Add valuable dev resources to increase our capacity
|
||||
@@ -59,7 +59,7 @@ Sprint velocity was very low due to holiday OOO
|
||||
|
||||
| | Goals | Impact |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Engineering | Continue login.gov [sandbox implementation](https://github.com/GSA/notifications-admin/issues/338), catch up deploys that lag from last week's dependency audit failures, change timezones displays to [Eastern time](https://github.com/GSA/notifications-admin/issues/861), Persist [individal send reports](https://github.com/GSA/notifications-admin/issues/855) somwehere in the app| Stronger auth security, streamlined UX, better timezone familiarity |
|
||||
| Engineering | Continue login.gov [sandbox implementation](https://github.com/GSA/notifications-admin/issues/338), catch up deploys that lag from last week's dependency audit failures, change timezones displays to [Eastern time](https://github.com/GSA/notifications-admin/issues/861), Persist [individual send reports](https://github.com/GSA/notifications-admin/issues/855) somwehere in the app| Stronger auth security, streamlined UX, better timezone familiarity |
|
||||
| UX | Continue making common-sense [UI/UX changes](https://github.com/GSA/notifications-admin/issues/828), review and finalize proposal for [Notify logo](https://github.com/GSA/notifications-admin/issues/859) & begin approvals chain | Improve ease of use, begin process of brand creation |
|
||||
| Security | Complete pre-requisite documentation, start project planning timelines and control group deadlines | Aim to have package completed with enough time to allow for long assessment|
|
||||
| Content | Finalize cloud.gov pages IAA mod for content site | Enable an easy static website for future content |
|
||||
@@ -117,7 +117,7 @@ Sprint velocity was very low due to holiday OOO
|
||||
| | Goals | Impact |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Engineering | (Due to low engineering availability, the goals from last sprint continue) Reduce [technical debt](https://github.com/GSA/notifications-api/issues/361) and stabilize application; carry out rigorous [testing](https://github.com/GSA/notifications-api/issues/312) of features and [infrastructure](https://github.com/GSA/notifications-api/issues/87); solve known [bugs](https://github.com/GSA/notifications-api/issues/360) | Increase app reliability prior to first partner use |
|
||||
| UX | Analyze data gathered from user-testing and propose changes from that analyzation; Hone a visual brand strategy for proposal to leadership | Improve upon oft-identified user barriers; create a consisten look and feel for Notify.gov |
|
||||
| UX | Analyze data gathered from user-testing and propose changes from that analyzation; Hone a visual brand strategy for proposal to leadership | Improve upon oft-identified user barriers; create a consistent look and feel for Notify.gov |
|
||||
| Security | Respond to any assessment questions and/or needs (assessment is nearing end) | Keep the LATO award timeline as short as possible|
|
||||
| Content | Collate research to inform future best practices content | Have content ready for later addition to the site
|
||||
|
||||
|
||||
40
gulpfile.js
40
gulpfile.js
@@ -47,10 +47,10 @@ const javascripts = () => {
|
||||
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
|
||||
paths.npm + 'jquery/dist/jquery.min.js',
|
||||
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
|
||||
paths.npm + 'timeago/jquery.timeago.js',
|
||||
paths.npm + 'textarea-caret/index.js',
|
||||
paths.npm + 'cbor-js/cbor.js',
|
||||
paths.npm + 'd3/dist/d3.min.js',
|
||||
paths.npm + 'socket.io-client/dist/socket.io.min.js'
|
||||
])
|
||||
);
|
||||
|
||||
@@ -58,7 +58,6 @@ const javascripts = () => {
|
||||
paths.toolkit + 'javascripts/govuk/modules.js',
|
||||
paths.toolkit + 'javascripts/govuk/show-hide-content.js',
|
||||
paths.src + 'javascripts/copyToClipboard.js',
|
||||
paths.src + 'javascripts/autofocus.js',
|
||||
paths.src + 'javascripts/enhancedTextbox.js',
|
||||
paths.src + 'javascripts/fileUpload.js',
|
||||
paths.src + 'javascripts/radioSelect.js',
|
||||
@@ -68,13 +67,12 @@ const javascripts = () => {
|
||||
paths.src + 'javascripts/errorTracking.js',
|
||||
paths.src + 'javascripts/preventDuplicateFormSubmissions.js',
|
||||
paths.src + 'javascripts/fullscreenTable.js',
|
||||
paths.src + 'javascripts/colourPreview.js',
|
||||
paths.src + 'javascripts/templateFolderForm.js',
|
||||
paths.src + 'javascripts/collapsibleCheckboxes.js',
|
||||
paths.src + 'javascripts/radioSlider.js',
|
||||
paths.src + 'javascripts/updateStatus.js',
|
||||
paths.src + 'javascripts/errorBanner.js',
|
||||
paths.src + 'javascripts/homepage.js',
|
||||
paths.src + 'javascripts/notifyModal.js',
|
||||
paths.src + 'javascripts/timeoutPopup.js',
|
||||
paths.src + 'javascripts/date.js',
|
||||
paths.src + 'javascripts/loginAlert.js',
|
||||
@@ -83,6 +81,7 @@ const javascripts = () => {
|
||||
paths.src + 'javascripts/activityChart.js',
|
||||
paths.src + 'javascripts/sidenav.js',
|
||||
paths.src + 'javascripts/validation.js',
|
||||
paths.src + 'javascripts/socketio.js',
|
||||
])
|
||||
.pipe(plugins.prettyerror())
|
||||
.pipe(
|
||||
@@ -121,8 +120,19 @@ const copyPDF = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const copyUSWDSJS = () => {
|
||||
return src('node_modules/@uswds/uswds/dist/js/uswds.min.js')
|
||||
.pipe(dest(paths.dist + 'js/'));
|
||||
};
|
||||
|
||||
// Configure USWDS paths
|
||||
uswds.settings.version = 3;
|
||||
uswds.settings.compile = {
|
||||
sass: true,
|
||||
javascript: true,
|
||||
sprites: false
|
||||
};
|
||||
|
||||
uswds.paths.dist.css = paths.dist + 'css';
|
||||
uswds.paths.dist.js = paths.dist + 'js';
|
||||
uswds.paths.dist.img = paths.dist + 'img';
|
||||
@@ -132,12 +142,23 @@ uswds.paths.dist.theme = paths.src + 'sass/uswds';
|
||||
|
||||
// Task to compile USWDS styles
|
||||
const styles = async () => {
|
||||
await uswds.compile();
|
||||
await uswds.compileSass();
|
||||
};
|
||||
|
||||
// Task to copy USWDS assets
|
||||
const copyAssets = async () => {
|
||||
await uswds.copyAssets();
|
||||
// Task to copy USWDS assetsconst
|
||||
const copyUSWDSAssets = () => {
|
||||
return src([
|
||||
'node_modules/@uswds/uswds/dist/img/**/*',
|
||||
'node_modules/@uswds/uswds/dist/fonts/**/*'
|
||||
], { encoding: false })
|
||||
.pipe(dest((file) => {
|
||||
if (file.path.includes('/img/')) {
|
||||
return paths.dist + 'img/';
|
||||
} else if (file.path.includes('/fonts/')) {
|
||||
return paths.dist + 'fonts/';
|
||||
}
|
||||
return paths.dist;
|
||||
}));
|
||||
};
|
||||
|
||||
// Optional backstopJS task
|
||||
@@ -174,7 +195,8 @@ exports.default = series(
|
||||
copySetTimezone,
|
||||
copyImages,
|
||||
copyPDF,
|
||||
copyAssets
|
||||
copyUSWDSAssets,
|
||||
copyUSWDSJS
|
||||
);
|
||||
exports.backstopTest = backstopTest;
|
||||
exports.backstopReference = backstopReference;
|
||||
|
||||
@@ -44,6 +44,7 @@ applications:
|
||||
|
||||
NOTIFY_BILLING_DETAILS: '[]'
|
||||
|
||||
SSL_CERT_FILE: '/etc/ssl/certs/ca-certificates.crt'
|
||||
REQUESTS_CA_BUNDLE: '/etc/ssl/certs/ca-certificates.crt'
|
||||
NEW_RELIC_CA_BUNDLE_PATH: '/etc/ssl/certs/ca-certificates.crt'
|
||||
|
||||
@@ -61,4 +62,7 @@ applications:
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: ((LOGIN_DOT_GOV_INITIAL_SIGNIN_URL))
|
||||
LOGIN_DOT_GOV_CERTS_URL: ((LOGIN_DOT_GOV_CERTS_URL))
|
||||
|
||||
API_PUBLIC_URL: ((API_PUBLIC_URL))
|
||||
API_PUBLIC_WS_URL: ((API_PUBLIC_WS_URL))
|
||||
# feature flagging
|
||||
FEATURE_SOCKET_ENABLED: ((FEATURE_SOCKET_ENABLED))
|
||||
|
||||
@@ -103,7 +103,7 @@ class RedisClient:
|
||||
|
||||
Notes:
|
||||
- Failed requests count. If over the limit and keep making requests you'll stay over the limit.
|
||||
- The actual value in the set is just the timestamp, the same as the score. We don't store any requets details.
|
||||
- The actual value in the set is just the timestamp, the same as the score. We don't store any request details.
|
||||
- return value of pipe.execute() is an array containing the outcome of each call.
|
||||
- result[2] == outcome of pipe.zcard()
|
||||
- If redis is inactive, or we get an exception, allow the request
|
||||
|
||||
@@ -338,7 +338,7 @@ def strip_and_remove_obscure_whitespace(value):
|
||||
|
||||
|
||||
def remove_whitespace(value):
|
||||
# Removes ALL whitespace, not just the obscure characters we normaly remove
|
||||
# Removes ALL whitespace, not just the obscure characters we normally remove
|
||||
for character in ALL_WHITESPACE:
|
||||
value = value.replace(character, "")
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from ordered_set import OrderedSet
|
||||
class InsensitiveDict(dict):
|
||||
"""
|
||||
`InsensitiveDict` behaves like an ordered dictionary, except it normalises
|
||||
case, whitespace, hypens and underscores in keys.
|
||||
case, whitespace, hyphens and underscores in keys.
|
||||
|
||||
In other words,
|
||||
InsensitiveDict({'FIRST_NAME': 'example'}) == InsensitiveDict({'first name': 'example'})
|
||||
@@ -23,7 +23,7 @@ class InsensitiveDict(dict):
|
||||
def from_keys(cls, keys):
|
||||
"""
|
||||
This behaves like `dict.from_keys`, except:
|
||||
- it normalises the keys to ignore case, whitespace, hypens and
|
||||
- it normalises the keys to ignore case, whitespace, hyphens and
|
||||
underscores
|
||||
- it stores the original, unnormalised key as the value of the
|
||||
item so it can be retrieved later
|
||||
|
||||
@@ -510,7 +510,7 @@
|
||||
'66':
|
||||
attributes:
|
||||
alpha: REG
|
||||
comment: Maximum long message lenght is 459 for GSM7 or 201 for Unicode alphabet.
|
||||
comment: Maximum long message length is 459 for GSM7 or 201 for Unicode alphabet.
|
||||
Special registration procedure for sending to DND numbers
|
||||
dlr: 'YES'
|
||||
generic_sender: SMS
|
||||
@@ -577,7 +577,7 @@
|
||||
sender_and_registration_info: All senders CONVERTED into one available national
|
||||
numeric sender
|
||||
text_restrictions: Content template MUST be approved by the MNO. Transactional
|
||||
traffic ONLY. Sufix added in the message text
|
||||
traffic ONLY. Suffix added in the message text
|
||||
billable_units: 1
|
||||
names:
|
||||
- China
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
import re
|
||||
import sys
|
||||
from itertools import product
|
||||
@@ -10,6 +9,21 @@ from flask.ctx import has_app_context, has_request_context
|
||||
from flask.logging import default_handler
|
||||
from pythonjsonlogger.jsonlogger import JsonFormatter as BaseJSONFormatter
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
COLOR_MAP = {
|
||||
"WARNING": "\033[93m",
|
||||
"ERROR": "\033[91m",
|
||||
"CRITICAL": "\033[95m",
|
||||
}
|
||||
RESET = "\033[0m"
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
color = self.COLOR_MAP.get(record.levelname, self.RESET)
|
||||
base_message = super().format(record)
|
||||
return f"{color}{base_message}{self.RESET}"
|
||||
|
||||
|
||||
LOG_FORMAT = (
|
||||
"%(asctime)s %(app_name)s %(name)s %(levelname)s "
|
||||
'%(request_id)s %(service_id)s "%(message)s" [in %(pathname)s:%(lineno)d]'
|
||||
@@ -79,7 +93,6 @@ def init_app(app):
|
||||
|
||||
def get_handlers(app):
|
||||
handlers = []
|
||||
standard_formatter = logging.Formatter(LOG_FORMAT, TIME_FORMAT)
|
||||
json_formatter = JSONFormatter(LOG_FORMAT, TIME_FORMAT)
|
||||
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
@@ -92,9 +105,9 @@ def get_handlers(app):
|
||||
return not ("GET /static/" in msg and " 200 " in msg)
|
||||
|
||||
logging.getLogger("werkzeug").addFilter(is_200_static_log)
|
||||
color_formatter = ColorFormatter(LOG_FORMAT, TIME_FORMAT)
|
||||
|
||||
# human readable stdout logs
|
||||
handlers.append(configure_handler(stream_handler, app, standard_formatter))
|
||||
handlers.append(configure_handler(stream_handler, app, color_formatter))
|
||||
|
||||
return handlers
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from flask import current_app
|
||||
import mistune
|
||||
from notifications_utils.formatters import create_sanitised_html_for_url
|
||||
import re
|
||||
import html
|
||||
import re
|
||||
|
||||
import mistune
|
||||
from flask import current_app
|
||||
|
||||
from notifications_utils.formatters import create_sanitised_html_for_url
|
||||
|
||||
LINK_STYLE = "word-wrap: break-word; color: #1D70B8;"
|
||||
|
||||
|
||||
@@ -652,7 +652,7 @@ def _do_simple_email_checks(match, email_address):
|
||||
def validate_email_address(email_address): # noqa (C901 too complex)
|
||||
# almost exactly the same as by https://github.com/wtforms/wtforms/blob/master/wtforms/validators.py,
|
||||
# with minor tweaks for SES compatibility - to avoid complications we are a lot stricter with the local part
|
||||
# than neccessary - we don't allow any double quotes or semicolons to prevent SES Technical Failures
|
||||
# than necessary - we don't allow any double quotes or semicolons to prevent SES Technical Failures
|
||||
email_address = strip_and_remove_obscure_whitespace(email_address)
|
||||
match = re.match(EMAIL_REGEX_PATTERN, email_address)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user