Compare commits
456 Commits
fixing-the
...
04-09-2024
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de0aed60f9 | ||
|
|
84bef51d7d | ||
|
|
d5d5a45138 | ||
|
|
6acbce0858 | ||
|
|
2b6b58c93a | ||
|
|
3e1c06f75c | ||
|
|
2ede41ce6a | ||
|
|
40efb4b561 | ||
|
|
51845ec0b0 | ||
|
|
2075400342 | ||
|
|
11b970adf8 | ||
|
|
14508b2d97 | ||
|
|
1f4573628f | ||
|
|
2cf3ddb6ff | ||
|
|
cc20c02839 | ||
|
|
2d7fba03f5 | ||
|
|
1c0c60a917 | ||
|
|
6b2ab54870 | ||
|
|
7f54232f21 | ||
|
|
8b17adeb34 | ||
|
|
c4611a291f | ||
|
|
616957c103 | ||
|
|
e411774ada | ||
|
|
748dbc12fd | ||
|
|
9021530a1a | ||
|
|
745532e657 | ||
|
|
32f36d433a | ||
|
|
cde0ab018a | ||
|
|
10ed1dab22 | ||
|
|
817c01faff | ||
|
|
acdd2806fc | ||
|
|
e572d789e0 | ||
|
|
e4990cfd70 | ||
|
|
23903b7d29 | ||
|
|
0fa9910e2c | ||
|
|
e5136d9fa7 | ||
|
|
a2ab04018d | ||
|
|
5854176ec8 | ||
|
|
3abe3352df | ||
|
|
f092ad8f98 | ||
|
|
2c57d0d373 | ||
|
|
bc79e4dd2c | ||
|
|
9b1c9b8ccf | ||
|
|
b29c2a3a62 | ||
|
|
245bcaecb3 | ||
|
|
2ca4c1b029 | ||
|
|
faaf602dea | ||
|
|
c6af0957e9 | ||
|
|
d8e9aaee0f | ||
|
|
4b9ec67345 | ||
|
|
6c1090ecb2 | ||
|
|
fd1ab3bf2e | ||
|
|
89e7de0ab6 | ||
|
|
e743ab41ff | ||
|
|
6d1efcadba | ||
|
|
07ee8dfe1e | ||
|
|
9d673a4d77 | ||
|
|
8ca32f476c | ||
|
|
0e0ee8ae0c | ||
|
|
4428df7dd5 | ||
|
|
9beb0a732c | ||
|
|
39c19594cd | ||
|
|
4af4fecd45 | ||
|
|
05563a7051 | ||
|
|
6e64df0c0b | ||
|
|
e24f77ec63 | ||
|
|
db5c259818 | ||
|
|
bf4f134538 | ||
|
|
1aa2501d1d | ||
|
|
addc078cbe | ||
|
|
8362c6b975 | ||
|
|
7c179cadca | ||
|
|
c83655c48c | ||
|
|
c4d738d70b | ||
|
|
a1736b5a4d | ||
|
|
b006e2cacf | ||
|
|
8e75110892 | ||
|
|
099345fe1e | ||
|
|
8afacfa89b | ||
|
|
ccd3e44eb1 | ||
|
|
6efb16120d | ||
|
|
24a0dd8c3b | ||
|
|
00f81a74fd | ||
|
|
c7821f2f73 | ||
|
|
04eaec6dd6 | ||
|
|
b12e638592 | ||
|
|
c053cc7402 | ||
|
|
e1611eebec | ||
|
|
4b1db40cbd | ||
|
|
197ef11075 | ||
|
|
b4986e4170 | ||
|
|
eb599344ad | ||
|
|
57fa8dad5d | ||
|
|
4e6f143675 | ||
|
|
27448d90a0 | ||
|
|
da5da1abe9 | ||
|
|
de003a8722 | ||
|
|
f391b5af2c | ||
|
|
e9b9072ae6 | ||
|
|
077b28bb15 | ||
|
|
10c9b86547 | ||
|
|
05495f4d42 | ||
|
|
ee1c4efb03 | ||
|
|
bc71549d12 | ||
|
|
6b31e5fba0 | ||
|
|
88c7f9590f | ||
|
|
dea7f28d5a | ||
|
|
0e608599c3 | ||
|
|
be2dc9a165 | ||
|
|
58a1d8e0c4 | ||
|
|
02ee1a81d1 | ||
|
|
c43ed20c9e | ||
|
|
18be5861d5 | ||
|
|
2afaca6ff1 | ||
|
|
f0eebfc4f2 | ||
|
|
cd06337574 | ||
|
|
85215f45a8 | ||
|
|
e4087ca9d5 | ||
|
|
3aacdcb3ef | ||
|
|
a09e813f80 | ||
|
|
476ba798fc | ||
|
|
ef53fe7e09 | ||
|
|
a05e09d345 | ||
|
|
04f76cd817 | ||
|
|
b2b6b4f612 | ||
|
|
d67923dafc | ||
|
|
69fce499c3 | ||
|
|
119b7fc832 | ||
|
|
4dc442c4b8 | ||
|
|
e35168bd40 | ||
|
|
590a599aa4 | ||
|
|
599652256e | ||
|
|
94e05705f8 | ||
|
|
b3998c532e | ||
|
|
2717feab7b | ||
|
|
ae084617fa | ||
|
|
e1981d3c65 | ||
|
|
37f6579ca8 | ||
|
|
cb0ba15571 | ||
|
|
6cdc14433b | ||
|
|
8d9c030d90 | ||
|
|
45c9018365 | ||
|
|
86a6b4ff8e | ||
|
|
7deaf71463 | ||
|
|
429a456704 | ||
|
|
0ae369d54b | ||
|
|
46ef759a92 | ||
|
|
ed5902b38e | ||
|
|
924ff82b03 | ||
|
|
bd7ef33c63 | ||
|
|
f10d854617 | ||
|
|
83d83469e4 | ||
|
|
7ff7455022 | ||
|
|
f0bd88b36a | ||
|
|
f5dbf6ad9e | ||
|
|
e9148851fb | ||
|
|
bdea60adb9 | ||
|
|
b489f49179 | ||
|
|
d1fe8a2332 | ||
|
|
7479a6c4ea | ||
|
|
c3bb44129c | ||
|
|
25c8026d89 | ||
|
|
2822b63a49 | ||
|
|
cb3fc0f039 | ||
|
|
335b01279b | ||
|
|
b1b416db8c | ||
|
|
ee5fe38001 | ||
|
|
58a067358e | ||
|
|
492f798274 | ||
|
|
99b2fdc28a | ||
|
|
5c39814daa | ||
|
|
f379f5de15 | ||
|
|
6a2f54367c | ||
|
|
7e2977f519 | ||
|
|
2be3db10d9 | ||
|
|
b3966ae800 | ||
|
|
f42cf3bd22 | ||
|
|
de38f38892 | ||
|
|
111c6fd93a | ||
|
|
c587bbee71 | ||
|
|
0af4545436 | ||
|
|
87b47d94df | ||
|
|
68163aa6d7 | ||
|
|
0bf289e23d | ||
|
|
5b8377faae | ||
|
|
9a62c9df27 | ||
|
|
ffd911b764 | ||
|
|
844f37bd6e | ||
|
|
026ed52ce5 | ||
|
|
4635b21c01 | ||
|
|
0d15734110 | ||
|
|
d0b37cabd6 | ||
|
|
8091a4afd6 | ||
|
|
cea10446f7 | ||
|
|
6e4018fc3d | ||
|
|
81d89f464a | ||
|
|
996db0d60b | ||
|
|
507973f9ae | ||
|
|
ebd04e44c2 | ||
|
|
98297515d8 | ||
|
|
7d8d988f62 | ||
|
|
dd37e277a5 | ||
|
|
51085cfc40 | ||
|
|
bdf22a1bc9 | ||
|
|
8661a22353 | ||
|
|
067b637f75 | ||
|
|
65f365ab6d | ||
|
|
f49f27bcdf | ||
|
|
602f2fccfd | ||
|
|
3775148a88 | ||
|
|
c514214fb0 | ||
|
|
116953b33f | ||
|
|
0ba6f6f708 | ||
|
|
a7db80ab04 | ||
|
|
d5f40dffe1 | ||
|
|
d3e4e2fd14 | ||
|
|
064c4d662e | ||
|
|
e9f9633287 | ||
|
|
2c6e02137b | ||
|
|
271086a04d | ||
|
|
48bf6800f0 | ||
|
|
3c525b18a5 | ||
|
|
598643b599 | ||
|
|
6ec0de4b63 | ||
|
|
f8b8dd5fb3 | ||
|
|
92fc35b6f6 | ||
|
|
4e32ef37b6 | ||
|
|
470574e197 | ||
|
|
58e252f092 | ||
|
|
f332587b37 | ||
|
|
f245c25e59 | ||
|
|
141c24e371 | ||
|
|
6d05c1ab75 | ||
|
|
fff440a6ff | ||
|
|
66909e9624 | ||
|
|
039b7cdece | ||
|
|
d08968a348 | ||
|
|
fd340ed105 | ||
|
|
0981990086 | ||
|
|
564fc73521 | ||
|
|
5c6a23c873 | ||
|
|
c891bc1b3f | ||
|
|
34d20c4d65 | ||
|
|
fff70d4edc | ||
|
|
2ee744e56d | ||
|
|
7fc5b88ea3 | ||
|
|
e71351d949 | ||
|
|
15c1863ab0 | ||
|
|
fadb8094f6 | ||
|
|
8ce0091cc8 | ||
|
|
df47a6ea8a | ||
|
|
25646c2e2f | ||
|
|
ffba252773 | ||
|
|
c71a7edcfe | ||
|
|
8e176d5cf5 | ||
|
|
673df7da3d | ||
|
|
5c0cd73d91 | ||
|
|
4741b381c2 | ||
|
|
d0c4b505b9 | ||
|
|
0a8794966e | ||
|
|
4180dca164 | ||
|
|
b50fe24c52 | ||
|
|
9c392bca78 | ||
|
|
7259d617ee | ||
|
|
fd5cd36959 | ||
|
|
3a6eeb2292 | ||
|
|
bd41dfc1c2 | ||
|
|
534daa2d82 | ||
|
|
c790d8b844 | ||
|
|
719ecfe8f6 | ||
|
|
1b6446e213 | ||
|
|
29c20a0ee9 | ||
|
|
5aacdcbc35 | ||
|
|
c2e7b4897d | ||
|
|
136403a263 | ||
|
|
aec20b5e73 | ||
|
|
95bdd335eb | ||
|
|
87ca03f6d8 | ||
|
|
9987946f92 | ||
|
|
e8e23e3ac0 | ||
|
|
997aa8e72f | ||
|
|
c652c17617 | ||
|
|
46be3cd7b3 | ||
|
|
2201c16a2b | ||
|
|
84559e0574 | ||
|
|
b9a7e48e78 | ||
|
|
558296684c | ||
|
|
3ba9a85845 | ||
|
|
57cd123f4e | ||
|
|
6b98cf7c35 | ||
|
|
91527f32f2 | ||
|
|
9c32fa2d4e | ||
|
|
b60dc00f0e | ||
|
|
92b33834e8 | ||
|
|
5e7319cbac | ||
|
|
d544d7c8f8 | ||
|
|
b8b330f55b | ||
|
|
bb242dc7d7 | ||
|
|
f8c48f7711 | ||
|
|
3003f82223 | ||
|
|
5a39165644 | ||
|
|
891cec5300 | ||
|
|
89b6e13afb | ||
|
|
d25f0869e0 | ||
|
|
4b39d5ffe9 | ||
|
|
1e60da1a37 | ||
|
|
164a414d22 | ||
|
|
759a90ca47 | ||
|
|
14b68c044f | ||
|
|
393dddea61 | ||
|
|
c20ec80753 | ||
|
|
96d5e72514 | ||
|
|
3363cc6979 | ||
|
|
4742cc77b5 | ||
|
|
60bcd9ddbd | ||
|
|
e3ff679b5e | ||
|
|
163596267a | ||
|
|
50511e1cea | ||
|
|
2e7aeb1dbf | ||
|
|
02d1c2b26e | ||
|
|
c383884017 | ||
|
|
442ff5dab2 | ||
|
|
44ec565d57 | ||
|
|
eccf8cb3e7 | ||
|
|
7471cefd2b | ||
|
|
d7606b2986 | ||
|
|
048872fb12 | ||
|
|
ddf017707f | ||
|
|
ebccd64242 | ||
|
|
72587524a7 | ||
|
|
53549495ea | ||
|
|
6c66fcb125 | ||
|
|
c4b8ba6c66 | ||
|
|
4eb886d8c0 | ||
|
|
c2d7828138 | ||
|
|
843e4bb2fa | ||
|
|
01eac988f1 | ||
|
|
49d270521a | ||
|
|
49c5f57d81 | ||
|
|
dd902468f2 | ||
|
|
ef5ed8ecab | ||
|
|
b034f1eb79 | ||
|
|
c74575a0b8 | ||
|
|
7b8a187db0 | ||
|
|
b7f21a6efd | ||
|
|
72aa14fd3b | ||
|
|
48f1c7c864 | ||
|
|
2aa9f2c532 | ||
|
|
b019dc3fe9 | ||
|
|
7832797628 | ||
|
|
fb469545b4 | ||
|
|
330fad469e | ||
|
|
456091996e | ||
|
|
3adce5842a | ||
|
|
72ab167265 | ||
|
|
bec550aca4 | ||
|
|
fbf902e168 | ||
|
|
15105bf506 | ||
|
|
8eb329b444 | ||
|
|
10d3a87bcb | ||
|
|
d6f012ee9e | ||
|
|
2962fc36c4 | ||
|
|
ad34ca0a11 | ||
|
|
df54b066f9 | ||
|
|
338a6426c3 | ||
|
|
f37903ff6d | ||
|
|
80135b2ff6 | ||
|
|
74d8ed8340 | ||
|
|
c073d2efac | ||
|
|
3ac65b122c | ||
|
|
802ebfa243 | ||
|
|
10f732b390 | ||
|
|
d82b56c644 | ||
|
|
93a36cce79 | ||
|
|
e18054d2d8 | ||
|
|
21c11adb5e | ||
|
|
3e52909e88 | ||
|
|
4e63a601e9 | ||
|
|
266b530e3a | ||
|
|
6af738e0de | ||
|
|
f97bfa863f | ||
|
|
0021d64253 | ||
|
|
27385bc45a | ||
|
|
f097b9ca07 | ||
|
|
50e3560c6f | ||
|
|
761c7bd0fd | ||
|
|
4da5c14751 | ||
|
|
0f167a91cc | ||
|
|
7c5674c4e1 | ||
|
|
6d3c92de28 | ||
|
|
e40a493f9b | ||
|
|
4e488fa079 | ||
|
|
5d480b3f53 | ||
|
|
070955175d | ||
|
|
a5eb311f83 | ||
|
|
6b4a5d009c | ||
|
|
1f12dd3908 | ||
|
|
0e64ff1c20 | ||
|
|
f7924320df | ||
|
|
1b4754386c | ||
|
|
a27519ff68 | ||
|
|
b72430af63 | ||
|
|
35b38f838c | ||
|
|
45ffa70c3c | ||
|
|
bd33fe4750 | ||
|
|
768f1986e8 | ||
|
|
e48cb94ccb | ||
|
|
0fc14131b1 | ||
|
|
6e05fd5147 | ||
|
|
c79c824b24 | ||
|
|
524a1e4687 | ||
|
|
9513069b42 | ||
|
|
af8309a2ce | ||
|
|
37086cb03b | ||
|
|
3ac9d44b2a | ||
|
|
d5210427f0 | ||
|
|
da1e2a2700 | ||
|
|
d9875ef287 | ||
|
|
cdd262365f | ||
|
|
195c68037b | ||
|
|
d5daab67cf | ||
|
|
782ec1c5eb | ||
|
|
fd8981d5cd | ||
|
|
1709d3279b | ||
|
|
2126ba8038 | ||
|
|
c9ed1ab59d | ||
|
|
a38518777c | ||
|
|
3379259e14 | ||
|
|
ede82d76d6 | ||
|
|
5970dd8055 | ||
|
|
65bccb3f95 | ||
|
|
75a4e1ae6d | ||
|
|
8baf9682a1 | ||
|
|
6b00cf1270 | ||
|
|
490174ab71 | ||
|
|
f15643c04d | ||
|
|
d253b601a2 | ||
|
|
9b0dc3dc3c | ||
|
|
443c7d9992 | ||
|
|
33433a3822 | ||
|
|
c7a393e620 | ||
|
|
ba3a229d60 | ||
|
|
17b1352f01 | ||
|
|
a1440ae36f | ||
|
|
d49fb8867e | ||
|
|
98b11dc3c4 | ||
|
|
6fe29a2bdd | ||
|
|
b463d420ef | ||
|
|
2a9f60c32e | ||
|
|
adda14342b | ||
|
|
d3c3183573 | ||
|
|
25305d0706 | ||
|
|
0e110201d6 | ||
|
|
11a85d688a | ||
|
|
82863b02ce | ||
|
|
27c74ca773 |
22
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
@@ -5,8 +5,6 @@ title: "Bug: [Brief description of the bug]"
|
||||
labels:
|
||||
- "Type: Bug"
|
||||
|
||||
assignees:
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -29,6 +27,17 @@ body:
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: In which environment did you experience the bug?
|
||||
options:
|
||||
- staging
|
||||
- demo
|
||||
- production
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: page
|
||||
attributes:
|
||||
@@ -36,6 +45,13 @@ body:
|
||||
description: please include URLs of where you saw the bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproSteps
|
||||
attributes:
|
||||
label: "Detail the steps for someone to reproduce"
|
||||
description: "Reproduction steps"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
@@ -52,4 +68,4 @@ body:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
required: false
|
||||
|
||||
|
||||
82
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: "QA Checklist"
|
||||
description: "Use this template to manually QA for prod push"
|
||||
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: Logging_in
|
||||
attributes:
|
||||
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: Choosing a service brings you to the dashboard for that service
|
||||
|
||||
- type: checkboxes
|
||||
id: send_template
|
||||
attributes:
|
||||
label: Create a template and send
|
||||
options:
|
||||
- label: Clicking “Send messages” brings you to templates page
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Uploading a csv is quick and automatically directs you to the preview/send page
|
||||
- label: Formatting looks clean and in place
|
||||
- label: Message dashboard populates and shows “pending” until call is made to AWS for delivery receipt
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__copied_template
|
||||
attributes:
|
||||
label: Send a message from copied template
|
||||
options:
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Clicking “copy an existing template” allows you to choose from pre-existing template
|
||||
- label: After editing, the new template saves and you prepare to send
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__existing_template
|
||||
attributes:
|
||||
label: Send a message from existing template
|
||||
options:
|
||||
- label: Clicking on existing template brings you to preview screen with `prepare to send` button
|
||||
- label: All else above persists as true (formatting, send, dashboard, etc)
|
||||
|
||||
- type: checkboxes
|
||||
id: download_report
|
||||
attributes:
|
||||
label: Download the csv delivery report
|
||||
options:
|
||||
- label: Clicking on the csv download directly after a message send downloads a report that contains the data we expect
|
||||
- label: The batch report appears on the main dashboard batch table with accurate information
|
||||
- label: Clicking on the csv download link from the main dashboard downloads a report that contains the data we expect
|
||||
|
||||
- type: checkboxes
|
||||
id: invite_member
|
||||
attributes:
|
||||
label: Invite a team member
|
||||
options:
|
||||
- label: Clicking on “Team” in settings brings you to team member screen
|
||||
- label: Clicking “Invite a team member” brings you to email and permissions screen
|
||||
- label: Clicking invite team member sends an invite and shows confirmation
|
||||
- label: Invite email provides live link that works to register new team member
|
||||
|
||||
- type: checkboxes
|
||||
id: delete_member
|
||||
attributes:
|
||||
label: Delete a team member
|
||||
options:
|
||||
- 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
|
||||
|
||||
6
.github/actions/setup-project/action.yml
vendored
@@ -9,10 +9,10 @@ runs:
|
||||
sudo apt-get update \
|
||||
&& sudo apt-get install -y --no-install-recommends \
|
||||
libcurl4-openssl-dev
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v3
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
python-version: "3.12"
|
||||
- name: Install poetry
|
||||
shell: bash
|
||||
run: pip install poetry
|
||||
|
||||
79
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
Please follow the instructions found in this pull request template so that we
|
||||
have all of the relevant details needed for our work.
|
||||
|
||||
At the minimum, please be sure to fill in all sections found below and also do
|
||||
the following:
|
||||
|
||||
- Provide an appropriate and descriptive title for the pull request
|
||||
- Link the pull request to its corresponding issue (must be done after creating
|
||||
the pull request itself)
|
||||
- Assign yourself as the author
|
||||
- Attach the appropriate labels to it
|
||||
- Set it to be on the Notify.gov project board
|
||||
- Select one or more reviewers from the team or mark the pull request as a draft
|
||||
depending on its current state
|
||||
- If the pull request is a draft, please be sure to add reviewers once it is
|
||||
ready for review and mark it ready for review
|
||||
|
||||
For each section, please delete the instructions/sample text (that includes this
|
||||
text, though it is wrapped in an HTML comment just in case) and put in your own
|
||||
information. Thank you!
|
||||
-->
|
||||
|
||||
*A note to PR reviewers: it may be helpful to review our
|
||||
[code review documentation](https://github.com/GSA/notifications-api/blob/main/docs/all.md#code-reviews)
|
||||
to know what to keep in mind while reviewing pull requests.*
|
||||
|
||||
## Description
|
||||
|
||||
Please enter a clear description about your proposed changes and what the
|
||||
expected outcome(s) is/are from there. If there are complex implementation
|
||||
details within the changes, this is a great place to explain those details using
|
||||
plain language.
|
||||
|
||||
This should include:
|
||||
|
||||
- Links to issues that this PR addresses
|
||||
- Screenshots or screen captures of any visible changes, especially for UI work
|
||||
- Dependency changes
|
||||
|
||||
If there are any caveats, known issues, follow-up items, etc., make a quick note
|
||||
of them here as well, though more details are probably warranted in the issue
|
||||
itself in this case.
|
||||
|
||||
## TODO (optional)
|
||||
|
||||
If you're opening a draft PR, it might be helpful to list any outstanding work,
|
||||
especially if you're asking folks to take a look before it's ready for full
|
||||
review. In this case, create a small checklist with the outstanding items:
|
||||
|
||||
- [ ] TODO item 1
|
||||
- [ ] TODO item 2
|
||||
- [ ] TODO item ...
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Please think about the security compliance aspect of your changes and what the
|
||||
potential impacts might be.
|
||||
|
||||
**NOTE: Please be mindful of sharing sensitive information here! If you're not
|
||||
sure of what to write, please ask the team first before writing anything here.**
|
||||
|
||||
Relevant details could include (and are not limited to) the following:
|
||||
|
||||
- Handling secrets/credential management (or specifically calling out that there
|
||||
is nothing to handle)
|
||||
- Any adjustments to the flow of data in and out the system, or even within it
|
||||
- Connecting or disconnecting any external services to the application
|
||||
- Handling of any sensitive information, such as PII
|
||||
- Handling of information within log statements or other application monitoring
|
||||
services/hooks
|
||||
- The inclusion of a new external dependency or the removal of an existing one
|
||||
- ... (anything else relevant from a security compliance perspective)
|
||||
|
||||
There are some cases where there are no security considerations to be had, e.g.,
|
||||
updating our documentation with publicly available information. In those cases
|
||||
it is fine to simply put something like this:
|
||||
|
||||
- None; this is a documentation update with publicly available information.
|
||||
127
.github/workflows/checks.yml
vendored
@@ -7,12 +7,12 @@ permissions:
|
||||
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: test
|
||||
NEW_RELIC_CONFIG_FILE: newrelic.ini
|
||||
NEW_RELIC_ENVIRONMENT: test
|
||||
FLASK_APP: application.py
|
||||
WERKZEUG_DEBUG_PIN: off
|
||||
REDIS_ENABLED: 0
|
||||
NODE_VERSION: 16.15.1
|
||||
AWS_US_TOLL_FREE_NUMBER: "+18556438890"
|
||||
ADMIN_BASE_URL: http://localhost:6012
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
@@ -44,38 +44,107 @@ jobs:
|
||||
run: npm test
|
||||
- name: Run py tests with coverage
|
||||
run: poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
# - name: Run E2E tests
|
||||
# run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
|
||||
# env:
|
||||
# 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: ${{ secrets.NOTIFY_E2E_TEST_URI }}
|
||||
- name: Check coverage threshold
|
||||
run: poetry run coverage report --fail-under=90
|
||||
# - name: Health check
|
||||
# run: |
|
||||
# response=$(curl -url ${{secrets.NOTIFY_E2E_TEST_URI}}_status)
|
||||
# if grep -q "ok" <<< "$response"; then
|
||||
# echo "Health check passed"
|
||||
# else
|
||||
# echo "Health check failed"
|
||||
# exit 1
|
||||
# fi
|
||||
# env:
|
||||
# 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: ${{ secrets.NOTIFY_E2E_TEST_URI }}
|
||||
|
||||
end-to-end-tests:
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
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: Clone API
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: GSA/notifications-api
|
||||
path: 'notifications-api'
|
||||
- name: Install API dependencies
|
||||
working-directory: 'notifications-api'
|
||||
run: make bootstrap
|
||||
env:
|
||||
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
|
||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||
REDIS_URL: redis://localhost:6379
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_ENVIRONMENT: development
|
||||
- name: Run API server
|
||||
working-directory: 'notifications-api'
|
||||
run: make run-procfile &
|
||||
env:
|
||||
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
|
||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||
REDIS_URL: redis://localhost:6379
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_ENVIRONMENT: development
|
||||
- name: Run Admin server
|
||||
run: make run-flask &
|
||||
env:
|
||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov
|
||||
API_HOST_NAME: http://localhost:6011
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: notify-admin
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012
|
||||
- name: Run E2E tests
|
||||
# Run the E2E tests against the code found in this PR.
|
||||
# run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
|
||||
# --browser webkit doesn't work at this time.
|
||||
run: make e2e-test
|
||||
# Debugging for now to troubleshoot a connectivity issue to the local servers
|
||||
# run: curl --request GET --url "http://localhost:6012"
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012
|
||||
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Validate NewRelic config
|
||||
env:
|
||||
NEW_RELIC_CONFIG_FILE: newrelic.ini
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
# Need to set a NEW_RELIC_ENVIRONMENT with monitor_mode: true
|
||||
NEW_RELIC_ENVIRONMENT: staging
|
||||
@@ -84,7 +153,7 @@ jobs:
|
||||
dependency-audits:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
@@ -97,7 +166,7 @@ jobs:
|
||||
static-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run scan
|
||||
run: poetry run bandit -r app/ --confidence-level medium
|
||||
@@ -105,7 +174,7 @@ jobs:
|
||||
dynamic-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run server
|
||||
run: make run-flask &
|
||||
@@ -124,7 +193,7 @@ jobs:
|
||||
a11y-scan:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run server
|
||||
run: make run-flask &
|
||||
|
||||
6
.github/workflows/daily_checks.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
dependency-audits:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
static-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run scan
|
||||
run: poetry run bandit -r app/ --confidence-level medium
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
dynamic-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run server
|
||||
run: make run-flask &
|
||||
|
||||
18
.github/workflows/deploy-demo.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: demo
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -54,7 +54,15 @@ jobs:
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
LOGIN_PEM: ${{ secrets.LOGIN_PEM }}
|
||||
LOGIN_DOT_GOV_CLIENT_ID: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov"
|
||||
LOGIN_DOT_GOV_USER_INFO_URL: "https://secure.login.gov/api/openid_connect/userinfo"
|
||||
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://secure.login.gov/api/openid_connect/token"
|
||||
LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://notify-demo.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-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"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -68,7 +76,15 @@ jobs:
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
--var LOGIN_DOT_GOV_LOGOUT_URL="$LOGIN_DOT_GOV_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
|
||||
19
.github/workflows/deploy-prod.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -54,8 +54,15 @@ jobs:
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
LOGIN_PEM: ${{ secrets.LOGIN_PEM }}
|
||||
|
||||
LOGIN_DOT_GOV_CLIENT_ID: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov"
|
||||
LOGIN_DOT_GOV_USER_INFO_URL: "https://secure.login.gov/api/openid_connect/userinfo"
|
||||
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://secure.login.gov/api/openid_connect/token"
|
||||
LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://beta.notify.gov/sign-out"
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://beta.notify.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -69,7 +76,15 @@ jobs:
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
--var LOGIN_DOT_GOV_LOGOUT_URL="$LOGIN_DOT_GOV_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
|
||||
14
.github/workflows/deploy.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -60,14 +60,15 @@ jobs:
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
LOGIN_PEM: ${{ secrets.LOGIN_PEM }}
|
||||
LOGIN_DOT_GOV_CLIENT_ID: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov"
|
||||
LOGIN_DOT_GOV_USER_INFO_URL: "https://idp.int.identitysandbox.gov/api/openid_connect/userinfo"
|
||||
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://idp.int.identitysandbox.gov/api/openid_connect/token"
|
||||
LOGIN_DOT_GOV_LOGOUT_URL: "https://idp.int.identitysandbox.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://idp.int.identitysandbox.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_USER_INFO_URL: "https://secure.login.gov/api/openid_connect/userinfo"
|
||||
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://secure.login.gov/api/openid_connect/token"
|
||||
LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://idp.int.identitysandbox.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=01234567890123456789012345&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=abcdefghijklmnopabcdefghijklmnop"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATEE"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -81,6 +82,7 @@ jobs:
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
|
||||
6
.github/workflows/drift.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
environment: demo
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'production'
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: 'production'
|
||||
|
||||
|
||||
2
.github/workflows/terraform-demo.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
environment: demo
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
|
||||
2
.github/workflows/terraform-production.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
|
||||
2
.github/workflows/terraform-staging.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
|
||||
3
.profile
@@ -4,3 +4,6 @@
|
||||
##
|
||||
|
||||
export NEW_RELIC_PROXY_HOST=$egress_proxy
|
||||
export http_proxy=$egress_proxy
|
||||
export https_proxy=$egress_proxy
|
||||
export no_proxy="apps.internal"
|
||||
|
||||
5
Makefile
@@ -17,6 +17,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
|
||||
bootstrap: generate-version-file ## Set up everything to run the app
|
||||
poetry self update
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
poetry run pre-commit install
|
||||
@@ -80,8 +81,8 @@ dead-code:
|
||||
|
||||
.PHONY: e2e-test
|
||||
e2e-test: export NEW_RELIC_ENVIRONMENT=test
|
||||
e2e-test: ## Run end-to-end integration tests
|
||||
poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
|
||||
e2e-test: ## Run end-to-end integration tests; note that --browser webkit isn't currently working
|
||||
poetry run pytest -v --browser chromium --browser firefox tests/end_to_end
|
||||
|
||||
.PHONY: js-lint
|
||||
js-lint: ## Run javascript linting scanners
|
||||
|
||||
93
README.md
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# Notify.gov Admin UI
|
||||
|
||||
This is the Notify front-end for government users and admins. To see it in
|
||||
@@ -183,12 +185,12 @@ session to make the changes take effect.
|
||||
Now we're ready to install the Python version we need with `pyenv`, like so:
|
||||
|
||||
```sh
|
||||
pyenv install 3.9
|
||||
pyenv install 3.12
|
||||
```
|
||||
|
||||
This will install the latest version of Python 3.9.
|
||||
This will install the latest version of Python 3.12.
|
||||
|
||||
_NOTE: This project currently runs on Python 3.9.x._
|
||||
_NOTE: This project currently runs on Python 3.12.x._
|
||||
|
||||
#### [API Step] Python Dependency Installation
|
||||
|
||||
@@ -232,7 +234,7 @@ Once all of pre-requisites for the project are installed and you have a
|
||||
cloud.gov account, you can now set up the admin project and get things running
|
||||
locally!
|
||||
|
||||
First, clone the respository in the directory of your choosing on your machine:
|
||||
First, clone the repository in the directory of your choosing on your machine:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:GSA/notifications-admin.git
|
||||
@@ -240,12 +242,12 @@ git clone git@github.com:GSA/notifications-admin.git
|
||||
|
||||
Now go into the project directory (`notifications-admin` by default), create a
|
||||
virtual environment, and set the local Python version to point to the virtual
|
||||
environment (assumes version Python `3.9.18` is what is installed on your
|
||||
environment (assumes version Python `3.12.2` is what is installed on your
|
||||
machine):
|
||||
|
||||
```sh
|
||||
cd notifications-admin
|
||||
pyenv virtualenv 3.9.18 notify-admin
|
||||
pyenv virtualenv 3.12.2 notify-admin
|
||||
pyenv local notify-admin
|
||||
```
|
||||
|
||||
@@ -272,6 +274,39 @@ In addition to some infrastructure setup, this will also create a local `.env`
|
||||
file for you in the project's root directory, which will include a handful of
|
||||
project-specific environment variables.
|
||||
|
||||
#### Upgrading Python in existing projects
|
||||
|
||||
If you're upgrading an existing project to a newer version of Python, you can
|
||||
follow these steps to get yourself up-to-date.
|
||||
|
||||
First, use `pyenv` to install the newer version of Python you'd like to use;
|
||||
we'll use `3.12` in our example here since we recently upgraded to this version:
|
||||
|
||||
```sh
|
||||
pyenv install 3.12
|
||||
```
|
||||
|
||||
Next, delete the virtual environment you previously had set up. If you followed
|
||||
the instructions above with the first-time set up, you can do this with `pyenv`:
|
||||
|
||||
```sh
|
||||
pyenv virtualenv-delete notify-admin
|
||||
```
|
||||
|
||||
Now, make sure you are in your project directory and recreate the same virtual
|
||||
environment with the newer version of Python you just installed:
|
||||
|
||||
```sh
|
||||
cd notifications-admin
|
||||
pyenv virtualenv 3.12.2 notify-admin
|
||||
pyenv local notify-admin
|
||||
```
|
||||
|
||||
At this point, proceed with the rest of the instructions here in the README and
|
||||
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`._
|
||||
|
||||
#### Updating the .env file for E2E tests
|
||||
|
||||
With the newly created `.env` file in place, you'll need to make one more
|
||||
@@ -378,52 +413,6 @@ You can do this by going through these steps:
|
||||
- Make a new PR with the change
|
||||
- Have the PR get reviewed and merged
|
||||
|
||||
### Python dependency management
|
||||
|
||||
We're using [`Poetry`](https://python-poetry.org/) for managing our Python
|
||||
dependencies and local virtual environments. When it comes to managing the
|
||||
Python dependencies, there are a couple of things to bear in mind.
|
||||
|
||||
For situations where you manually manipulate the `pyproject.toml` file, you
|
||||
should use the `make py-lock` command to sync the `poetry.lock` file. This will
|
||||
ensure that you don't inadvertently bring in other transitive dependency updates
|
||||
that have not been fully tested with the project yet.
|
||||
|
||||
If you're just trying to update a dependency to a newer (or the latest) version,
|
||||
you should let Poetry take care of that for you by running the following:
|
||||
|
||||
```sh
|
||||
poetry update <dependency> [<dependency>...]
|
||||
```
|
||||
|
||||
You can specify more than one dependency together. With this command, Poetry
|
||||
will do the following for you:
|
||||
|
||||
- Find the latest compatible version(s) of the specified dependency/dependencies
|
||||
- Install the new versions
|
||||
- Update and sync the `poetry.lock` file
|
||||
|
||||
In either situation, once you are finished and have verified the dependency
|
||||
changes are working, please be sure to commit both the `pyproject.toml` and
|
||||
`poetry.lock` files.
|
||||
|
||||
### Keeping the notification-utils dependency up-to-date
|
||||
|
||||
The `notifications-utils` dependency references the other repository we have at
|
||||
https://github.com/GSA/notifications-utils - this dependency requires a bit of
|
||||
extra legwork to ensure it stays up-to-date.
|
||||
|
||||
Whenever a PR is merged in the `notifications-utils` repository, we need to make
|
||||
sure the changes are pulled in here and committed to this repository as well.
|
||||
You can do this by going through these steps:
|
||||
|
||||
- Make sure your local `main` branch is up-to-date
|
||||
- Create a new branch to work in
|
||||
- Run `make update-utils`
|
||||
- Commit the updated `poetry.lock` file and push the changes
|
||||
- Make a new PR with the change
|
||||
- Have the PR get reviewed and merged
|
||||
|
||||
## Known Installation Issues
|
||||
|
||||
### Python Installation Errors
|
||||
|
||||
@@ -21,7 +21,6 @@ from flask_login import LoginManager, current_user
|
||||
from flask_talisman import Talisman
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_wtf.csrf import CSRFError
|
||||
from govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
||||
from itsdangerous import BadSignature
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils import logging, request_helper
|
||||
@@ -53,7 +52,7 @@ from app.formatters import (
|
||||
format_datetime_human,
|
||||
format_datetime_normal,
|
||||
format_datetime_relative,
|
||||
format_datetime_short,
|
||||
format_datetime_table,
|
||||
format_day_of_week,
|
||||
format_delta,
|
||||
format_delta_days,
|
||||
@@ -113,6 +112,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.govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
||||
|
||||
login_manager = LoginManager()
|
||||
csrf = CSRFProtect()
|
||||
@@ -380,6 +380,10 @@ def load_service_before_request():
|
||||
request_ctx.service = Service(
|
||||
service_api_client.get_service(service_id)["data"]
|
||||
)
|
||||
stats = service_api_client.get_service_statistics(
|
||||
service_id, limit_days=7
|
||||
)
|
||||
request_ctx.service.stats = stats
|
||||
except HTTPError as exc:
|
||||
# if service id isn't real, then 404 rather than 500ing later because we expect service to be set
|
||||
if exc.status_code == 404:
|
||||
@@ -547,7 +551,7 @@ def add_template_filters(application):
|
||||
format_datetime,
|
||||
format_datetime_24h,
|
||||
format_datetime_normal,
|
||||
format_datetime_short,
|
||||
format_datetime_table,
|
||||
valid_phone_number,
|
||||
linkable_name,
|
||||
format_date,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<link rel="stylesheet" media="screen" href="/static/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
|
||||
<link rel="stylesheet" media="print" href="/static/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
|
||||
|
||||
<meta property="og:image" content="/static/images/usa-opengraph-image.png">
|
||||
<meta property="og:image" content="/static/images/notify-og-image.png">
|
||||
</head>
|
||||
|
||||
<body class="usa-template__body">
|
||||
@@ -107,4 +107,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 6.0 KiB |
BIN
app/assets/images/android-chrome-256x256.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 994 B |
|
Before Width: | Height: | Size: 384 B After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
app/assets/images/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
7
app/assets/images/notify-bell.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="187" height="230" viewBox="0 0 187 230" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M89.1324 230V203.125H57.936C60.0752 226.499 79.7735 230 89.1324 230Z" fill="#2491FF"/>
|
||||
<path d="M89.1324 230V203.125H121.22C119.02 226.499 98.7587 230 89.1324 230Z" fill="#005EA2"/>
|
||||
<path d="M71.4869 19.5185C71.4869 3.35574 82.3456 0 89.1324 0V194.166H0V174.861L22.1703 153.759V87.3122C22.1703 47.8033 53.8415 24.1578 71.4869 19.5185Z" fill="#2491FF"/>
|
||||
<path d="M89.1324 0C95.9192 0 106.778 3.35574 106.778 19.5185C112.193 20.9422 118.928 24.1557 125.664 29.0681C118.405 36.2889 113.911 46.2873 113.911 57.3353C113.911 79.3543 131.761 97.2043 153.78 97.2043C154.557 97.2043 155.293 97.2481 156.058 97.2043L156.094 153.759L178.265 174.861V194.166H89.1324V0Z" fill="#005EA2"/>
|
||||
<path d="M186.435 57.1535C186.435 74.9784 171.985 89.4283 154.16 89.4283C136.335 89.4283 121.885 74.9784 121.885 57.1535C121.885 39.3286 136.335 24.8786 154.16 24.8786C171.985 24.8786 186.435 39.3286 186.435 57.1535Z" fill="#D54309"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
app/assets/images/notify-dark-favicon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
17
app/assets/images/notify-logo-dark.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="1596" height="400" viewBox="0 0 1596 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M155.013 400V353.26H100.758C104.479 393.912 138.736 400 155.013 400Z" fill="#58B4FF"/>
|
||||
<path d="M155.013 400V353.26H210.818C206.991 393.912 171.754 400 155.013 400Z" fill="#0076D6"/>
|
||||
<path d="M124.325 33.9452C124.325 5.83608 143.21 0 155.013 0V337.68H0V304.105L38.5571 267.407V151.847C38.5571 83.1361 93.6374 42.0136 124.325 33.9452Z" fill="#58B4FF"/>
|
||||
<path d="M155.013 0C166.816 0 185.701 5.83608 185.701 33.9452C195.118 36.4212 206.832 42.0098 218.547 50.5533C205.922 63.1111 198.106 80.4997 198.106 99.7136C198.106 138.008 229.15 169.051 267.444 169.051C268.795 169.051 270.074 169.127 271.406 169.051L271.469 267.407L310.026 304.105V337.68H155.013V0Z" fill="#0076D6"/>
|
||||
<path d="M324.234 99.3973C324.234 130.397 299.104 155.528 268.104 155.528C237.104 155.528 211.974 130.397 211.974 99.3973C211.974 68.3975 237.104 43.2672 268.104 43.2672C299.104 43.2672 324.234 68.3975 324.234 99.3973Z" fill="#D54309"/>
|
||||
<path d="M416.684 290.469C414.747 290.469 413.25 289.94 412.194 288.884C411.137 287.651 410.609 286.154 410.609 284.393V111.909C410.609 109.972 411.137 108.475 412.194 107.418C413.25 106.186 414.747 105.569 416.684 105.569H428.835C430.948 105.569 432.532 106.098 433.589 107.154C434.646 108.035 435.262 108.739 435.438 109.267L523.133 243.98V111.909C523.133 109.972 523.662 108.475 524.718 107.418C525.775 106.186 527.271 105.569 529.208 105.569H541.887C543.824 105.569 545.321 106.186 546.378 107.418C547.61 108.475 548.227 109.972 548.227 111.909V284.129C548.227 285.89 547.61 287.387 546.378 288.62C545.321 289.852 543.912 290.469 542.151 290.469H529.473C527.536 290.469 526.039 289.94 524.982 288.884C524.102 287.827 523.485 287.123 523.133 286.771L435.702 153.379V284.393C435.702 286.154 435.086 287.651 433.853 288.884C432.797 289.94 431.3 290.469 429.363 290.469H416.684Z" fill="#EDEFF0"/>
|
||||
<path d="M636.275 293.11C622.892 293.11 611.71 290.557 602.729 285.45C593.748 280.343 586.88 273.299 582.126 264.319C577.371 255.162 574.73 244.772 574.201 233.15C574.025 230.156 573.937 226.37 573.937 221.792C573.937 217.037 574.025 213.251 574.201 210.434C574.73 198.635 577.371 188.246 582.126 179.265C587.056 170.284 594.012 163.24 602.993 158.133C611.974 153.027 623.068 150.473 636.275 150.473C649.482 150.473 660.576 153.027 669.557 158.133C678.537 163.24 685.405 170.284 690.16 179.265C695.09 188.246 697.82 198.635 698.348 210.434C698.524 213.251 698.612 217.037 698.612 221.792C698.612 226.37 698.524 230.156 698.348 233.15C697.82 244.772 695.178 255.162 690.424 264.319C685.669 273.299 678.802 280.343 669.821 285.45C660.84 290.557 649.658 293.11 636.275 293.11ZM636.275 272.771C647.193 272.771 655.909 269.337 662.425 262.47C668.94 255.426 672.462 245.212 672.99 231.829C673.167 229.188 673.255 225.842 673.255 221.792C673.255 217.742 673.167 214.396 672.99 211.754C672.462 198.371 668.94 188.246 662.425 181.378C655.909 174.334 647.193 170.812 636.275 170.812C625.357 170.812 616.552 174.334 609.861 181.378C603.345 188.246 599.911 198.371 599.559 211.754C599.383 214.396 599.295 217.742 599.295 221.792C599.295 225.842 599.383 229.188 599.559 231.829C599.911 245.212 603.345 255.426 609.861 262.47C616.552 269.337 625.357 272.771 636.275 272.771Z" fill="#EDEFF0"/>
|
||||
<path d="M781.377 290.469C771.516 290.469 763.504 288.62 757.341 284.922C751.177 281.048 746.687 275.677 743.869 268.809C741.052 261.765 739.643 253.489 739.643 243.98V174.246H719.04C717.279 174.246 715.782 173.718 714.549 172.661C713.493 171.429 712.965 169.932 712.965 168.171V159.19C712.965 157.429 713.493 156.02 714.549 154.964C715.782 153.731 717.279 153.115 719.04 153.115H739.643V109.003C739.643 107.242 740.171 105.833 741.228 104.777C742.461 103.544 743.957 102.928 745.718 102.928H758.133C759.894 102.928 761.303 103.544 762.359 104.777C763.592 105.833 764.208 107.242 764.208 109.003V153.115H796.962C798.723 153.115 800.132 153.731 801.188 154.964C802.421 156.02 803.037 157.429 803.037 159.19V168.171C803.037 169.932 802.421 171.429 801.188 172.661C800.132 173.718 798.723 174.246 796.962 174.246H764.208V242.131C764.208 250.407 765.617 256.923 768.434 261.677C771.252 266.432 776.271 268.809 783.491 268.809H799.603C801.364 268.809 802.773 269.425 803.83 270.658C805.062 271.715 805.678 273.123 805.678 274.884V284.393C805.678 286.154 805.062 287.651 803.83 288.884C802.773 289.94 801.364 290.469 799.603 290.469H781.377Z" fill="#EDEFF0"/>
|
||||
<path d="M834.371 290.469C832.61 290.469 831.113 289.94 829.88 288.884C828.824 287.651 828.296 286.154 828.296 284.393V159.19C828.296 157.429 828.824 156.02 829.88 154.964C831.113 153.731 832.61 153.115 834.371 153.115H847.05C848.811 153.115 850.219 153.731 851.276 154.964C852.333 156.02 852.861 157.429 852.861 159.19V284.393C852.861 286.154 852.333 287.651 851.276 288.884C850.219 289.94 848.811 290.469 847.05 290.469H834.371ZM832.258 127.493C830.497 127.493 829 126.965 827.767 125.908C826.711 124.675 826.182 123.179 826.182 121.418V107.154C826.182 105.393 826.711 103.984 827.767 102.928C829 101.695 830.497 101.079 832.258 101.079H848.899C850.66 101.079 852.068 101.695 853.125 102.928C854.358 103.984 854.974 105.393 854.974 107.154V121.418C854.974 123.179 854.358 124.675 853.125 125.908C852.068 126.965 850.66 127.493 848.899 127.493H832.258Z" fill="#EDEFF0"/>
|
||||
<path d="M912.078 290.469C910.317 290.469 908.82 289.94 907.588 288.884C906.531 287.651 906.003 286.154 906.003 284.393V174.246H883.551C881.79 174.246 880.293 173.718 879.06 172.661C878.004 171.429 877.476 169.932 877.476 168.171V159.19C877.476 157.429 878.004 156.02 879.06 154.964C880.293 153.731 881.79 153.115 883.551 153.115H906.003V139.908C906.003 130.927 907.5 123.091 910.493 116.399C913.487 109.531 918.242 104.249 924.757 100.551C931.449 96.8525 940.341 95.0036 951.435 95.0036H967.284C969.045 95.0036 970.454 95.6199 971.51 96.8525C972.567 97.9091 973.095 99.3179 973.095 101.079V110.06C973.095 111.821 972.567 113.317 971.51 114.55C970.454 115.607 969.045 116.135 967.284 116.135H951.964C943.687 116.135 938.052 118.336 935.059 122.738C932.065 126.965 930.568 133.128 930.568 141.228V153.115H964.642C966.403 153.115 967.812 153.731 968.869 154.964C969.925 156.02 970.454 157.429 970.454 159.19V168.171C970.454 169.932 969.925 171.429 968.869 172.661C967.812 173.718 966.403 174.246 964.642 174.246H930.568V284.393C930.568 286.154 929.952 287.651 928.719 288.884C927.663 289.94 926.254 290.469 924.493 290.469H912.078Z" fill="#EDEFF0"/>
|
||||
<path d="M1017.94 340.656C1016.53 340.656 1015.3 340.127 1014.24 339.071C1013.18 338.014 1012.65 336.782 1012.65 335.373C1012.65 334.668 1012.74 333.964 1012.92 333.26C1013.09 332.555 1013.45 331.675 1013.98 330.618L1034.31 282.28L983.863 163.152C982.982 161.039 982.542 159.542 982.542 158.662C982.542 157.077 983.071 155.756 984.127 154.7C985.184 153.643 986.504 153.115 988.089 153.115H1001.03C1002.79 153.115 1004.2 153.555 1005.26 154.435C1006.31 155.316 1007.02 156.373 1007.37 157.605L1047.52 253.753L1088.73 157.605C1089.26 156.373 1089.96 155.316 1090.84 154.435C1091.9 153.555 1093.39 153.115 1095.33 153.115H1107.75C1109.33 153.115 1110.65 153.643 1111.71 154.7C1112.76 155.756 1113.29 156.989 1113.29 158.398C1113.29 159.278 1112.85 160.863 1111.97 163.152L1036.96 336.165C1036.43 337.398 1035.63 338.454 1034.58 339.335C1033.7 340.215 1032.29 340.656 1030.35 340.656H1017.94Z" fill="#EDEFF0"/>
|
||||
<path d="M1128.65 290.469C1126.89 290.469 1125.39 289.94 1124.16 288.884C1123.1 287.651 1122.57 286.154 1122.57 284.393V265.375C1122.57 263.614 1123.1 262.205 1124.16 261.149C1125.39 259.916 1126.89 259.3 1128.65 259.3H1147.67C1149.43 259.3 1150.84 259.916 1151.89 261.149C1153.13 262.205 1153.74 263.614 1153.74 265.375V284.393C1153.74 286.154 1153.13 287.651 1151.89 288.884C1150.84 289.94 1149.43 290.469 1147.67 290.469H1128.65Z" fill="#EDEFF0"/>
|
||||
<path d="M1244.66 348.58C1232.68 348.58 1222.82 346.995 1215.07 343.825C1207.5 340.656 1201.51 336.782 1197.11 332.203C1192.89 327.801 1189.8 323.31 1187.87 318.732C1186.11 314.329 1185.14 310.808 1184.96 308.166C1184.79 306.405 1185.31 304.908 1186.55 303.676C1187.96 302.443 1189.45 301.827 1191.04 301.827H1203.72C1205.3 301.827 1206.62 302.179 1207.68 302.883C1208.73 303.588 1209.61 305.084 1210.32 307.374C1211.38 310.015 1212.96 312.921 1215.07 316.09C1217.19 319.436 1220.44 322.254 1224.85 324.543C1229.25 327.008 1235.41 328.241 1243.34 328.241C1251.61 328.241 1258.57 327.096 1264.2 324.807C1269.84 322.694 1274.07 318.82 1276.88 313.185C1279.88 307.726 1281.37 300.154 1281.37 290.469V272.507C1277.15 277.966 1271.6 282.544 1264.73 286.242C1257.86 289.764 1249.24 291.525 1238.85 291.525C1228.99 291.525 1220.53 289.852 1213.49 286.507C1206.45 282.985 1200.63 278.318 1196.06 272.507C1191.65 266.52 1188.31 259.74 1186.02 252.168C1183.91 244.596 1182.67 236.672 1182.32 228.395C1182.14 223.465 1182.14 218.622 1182.32 213.867C1182.67 205.591 1183.91 197.667 1186.02 190.095C1188.31 182.523 1191.65 175.743 1196.06 169.756C1200.63 163.769 1206.45 159.102 1213.49 155.756C1220.53 152.234 1228.99 150.473 1238.85 150.473C1249.41 150.473 1258.13 152.498 1265 156.549C1272.04 160.423 1277.76 165.177 1282.17 170.812V159.454C1282.17 157.693 1282.69 156.196 1283.75 154.964C1284.98 153.731 1286.48 153.115 1288.24 153.115H1300.39C1302.15 153.115 1303.65 153.731 1304.88 154.964C1306.11 156.196 1306.73 157.693 1306.73 159.454V287.563C1306.73 299.361 1304.71 309.839 1300.66 318.996C1296.78 328.153 1290.27 335.373 1281.11 340.656C1272.13 345.938 1259.98 348.58 1244.66 348.58ZM1244.13 270.394C1252.93 270.394 1259.98 268.369 1265.26 264.319C1270.72 260.268 1274.68 255.162 1277.15 248.998C1279.79 242.835 1281.2 236.584 1281.37 230.244C1281.55 227.779 1281.64 224.785 1281.64 221.263C1281.64 217.565 1281.55 214.484 1281.37 212.018C1281.2 205.679 1279.79 199.428 1277.15 193.264C1274.68 187.101 1270.72 181.994 1265.26 177.944C1259.98 173.894 1252.93 171.869 1244.13 171.869C1235.32 171.869 1228.28 173.894 1223 177.944C1217.72 181.818 1213.93 187.013 1211.64 193.529C1209.35 199.868 1208.03 206.736 1207.68 214.132C1207.5 218.71 1207.5 223.377 1207.68 228.131C1208.03 235.527 1209.35 242.483 1211.64 248.998C1213.93 255.338 1217.72 260.533 1223 264.583C1228.28 268.457 1235.32 270.394 1244.13 270.394Z" fill="#EDEFF0"/>
|
||||
<path d="M1394.34 293.11C1380.96 293.11 1369.78 290.557 1360.8 285.45C1351.81 280.343 1344.95 273.299 1340.19 264.319C1335.44 255.162 1332.8 244.772 1332.27 233.15C1332.09 230.156 1332 226.37 1332 221.792C1332 217.037 1332.09 213.251 1332.27 210.434C1332.8 198.635 1335.44 188.246 1340.19 179.265C1345.12 170.284 1352.08 163.24 1361.06 158.133C1370.04 153.027 1381.13 150.473 1394.34 150.473C1407.55 150.473 1418.64 153.027 1427.62 158.133C1436.6 163.24 1443.47 170.284 1448.23 179.265C1453.16 188.246 1455.89 198.635 1456.42 210.434C1456.59 213.251 1456.68 217.037 1456.68 221.792C1456.68 226.37 1456.59 230.156 1456.42 233.15C1455.89 244.772 1453.25 255.162 1448.49 264.319C1443.74 273.299 1436.87 280.343 1427.89 285.45C1418.91 290.557 1407.72 293.11 1394.34 293.11ZM1394.34 272.771C1405.26 272.771 1413.98 269.337 1420.49 262.47C1427.01 255.426 1430.53 245.212 1431.06 231.829C1431.23 229.188 1431.32 225.842 1431.32 221.792C1431.32 217.742 1431.23 214.396 1431.06 211.754C1430.53 198.371 1427.01 188.246 1420.49 181.378C1413.98 174.334 1405.26 170.812 1394.34 170.812C1383.42 170.812 1374.62 174.334 1367.93 181.378C1361.41 188.246 1357.98 198.371 1357.63 211.754C1357.45 214.396 1357.36 217.742 1357.36 221.792C1357.36 225.842 1357.45 229.188 1357.63 231.829C1357.98 245.212 1361.41 255.426 1367.93 262.47C1374.62 269.337 1383.42 272.771 1394.34 272.771Z" fill="#EDEFF0"/>
|
||||
<path d="M1524.74 290.469C1522.1 290.469 1519.98 289.852 1518.4 288.62C1516.99 287.387 1515.85 285.802 1514.97 283.865L1466.1 161.303C1465.75 160.423 1465.57 159.542 1465.57 158.662C1465.57 157.077 1466.1 155.756 1467.16 154.7C1468.21 153.643 1469.53 153.115 1471.12 153.115H1483.8C1485.73 153.115 1487.23 153.643 1488.29 154.7C1489.34 155.756 1489.96 156.725 1490.14 157.605L1530.55 261.677L1570.7 157.605C1571.05 156.725 1571.67 155.756 1572.55 154.7C1573.61 153.643 1575.1 153.115 1577.04 153.115H1589.98C1591.39 153.115 1592.62 153.643 1593.68 154.7C1594.91 155.756 1595.53 157.077 1595.53 158.662C1595.53 159.542 1595.35 160.423 1595 161.303L1545.87 283.865C1545.17 285.802 1544.02 287.387 1542.44 288.62C1541.03 289.852 1538.91 290.469 1536.1 290.469H1524.74Z" fill="#EDEFF0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
BIN
app/assets/images/notify-logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
17
app/assets/images/notify-logo.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="1596" height="400" viewBox="0 0 1596 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M155.013 400V353.26H100.759C104.479 393.912 138.737 400 155.013 400Z" fill="#2491FF"/>
|
||||
<path d="M155.013 400V353.26H210.818C206.992 393.912 171.755 400 155.013 400Z" fill="#005EA2"/>
|
||||
<path d="M124.326 33.9452C124.326 5.83608 143.21 0 155.014 0V337.68H0V304.105L38.5573 267.407V151.847C38.5573 83.1361 93.6378 42.0136 124.326 33.9452Z" fill="#2491FF"/>
|
||||
<path d="M155.014 0C166.817 0 185.702 5.83608 185.702 33.9452C195.119 36.4212 206.833 42.0098 218.548 50.5533C205.923 63.1111 198.107 80.4997 198.107 99.7136C198.107 138.008 229.151 169.051 267.445 169.051C268.796 169.051 270.075 169.127 271.407 169.051L271.47 267.407L310.027 304.105V337.68H155.014V0Z" fill="#005EA2"/>
|
||||
<path d="M324.236 99.3973C324.236 130.397 299.105 155.528 268.105 155.528C237.105 155.528 211.975 130.397 211.975 99.3973C211.975 68.3975 237.105 43.2672 268.105 43.2672C299.105 43.2672 324.236 68.3975 324.236 99.3973Z" fill="#D54309"/>
|
||||
<path d="M416.686 290.469C414.749 290.469 413.252 289.94 412.195 288.884C411.139 287.651 410.61 286.154 410.61 284.393V111.909C410.61 109.972 411.139 108.475 412.195 107.418C413.252 106.186 414.749 105.569 416.686 105.569H428.836C430.95 105.569 432.534 106.098 433.591 107.154C434.647 108.035 435.264 108.739 435.44 109.267L523.135 243.98V111.909C523.135 109.972 523.664 108.475 524.72 107.418C525.777 106.186 527.274 105.569 529.211 105.569H541.89C543.827 105.569 545.323 106.186 546.38 107.418C547.613 108.475 548.229 109.972 548.229 111.909V284.129C548.229 285.89 547.613 287.387 546.38 288.62C545.323 289.852 543.915 290.469 542.154 290.469H529.475C527.538 290.469 526.041 289.94 524.984 288.884C524.104 287.827 523.488 287.123 523.135 286.771L435.704 153.379V284.393C435.704 286.154 435.088 287.651 433.855 288.884C432.798 289.94 431.302 290.469 429.365 290.469H416.686Z" fill="#2D2E2F"/>
|
||||
<path d="M636.277 293.11C622.894 293.11 611.712 290.557 602.731 285.45C593.75 280.343 586.883 273.299 582.128 264.319C577.373 255.162 574.732 244.772 574.204 233.15C574.028 230.156 573.94 226.37 573.94 221.792C573.94 217.037 574.028 213.251 574.204 210.434C574.732 198.635 577.373 188.246 582.128 179.265C587.059 170.284 594.014 163.24 602.995 158.133C611.976 153.027 623.07 150.473 636.277 150.473C649.485 150.473 660.579 153.027 669.559 158.133C678.54 163.24 685.408 170.284 690.163 179.265C695.093 188.246 697.823 198.635 698.351 210.434C698.527 213.251 698.615 217.037 698.615 221.792C698.615 226.37 698.527 230.156 698.351 233.15C697.823 244.772 695.181 255.162 690.427 264.319C685.672 273.299 678.804 280.343 669.824 285.45C660.843 290.557 649.661 293.11 636.277 293.11ZM636.277 272.771C647.195 272.771 655.912 269.337 662.428 262.47C668.943 255.426 672.465 245.212 672.993 231.829C673.169 229.188 673.257 225.842 673.257 221.792C673.257 217.742 673.169 214.396 672.993 211.754C672.465 198.371 668.943 188.246 662.428 181.378C655.912 174.334 647.195 170.812 636.277 170.812C625.359 170.812 616.555 174.334 609.863 181.378C603.348 188.246 599.914 198.371 599.562 211.754C599.385 214.396 599.297 217.742 599.297 221.792C599.297 225.842 599.385 229.188 599.562 231.829C599.914 245.212 603.348 255.426 609.863 262.47C616.555 269.337 625.359 272.771 636.277 272.771Z" fill="#2D2E2F"/>
|
||||
<path d="M781.381 290.469C771.519 290.469 763.507 288.62 757.344 284.922C751.18 281.048 746.69 275.677 743.872 268.809C741.055 261.765 739.646 253.489 739.646 243.98V174.246H719.043C717.282 174.246 715.785 173.718 714.552 172.661C713.496 171.429 712.968 169.932 712.968 168.171V159.19C712.968 157.429 713.496 156.02 714.552 154.964C715.785 153.731 717.282 153.115 719.043 153.115H739.646V109.003C739.646 107.242 740.174 105.833 741.231 104.777C742.464 103.544 743.96 102.928 745.721 102.928H758.136C759.897 102.928 761.306 103.544 762.362 104.777C763.595 105.833 764.211 107.242 764.211 109.003V153.115H796.965C798.726 153.115 800.135 153.731 801.191 154.964C802.424 156.02 803.04 157.429 803.04 159.19V168.171C803.04 169.932 802.424 171.429 801.191 172.661C800.135 173.718 798.726 174.246 796.965 174.246H764.211V242.131C764.211 250.407 765.62 256.923 768.438 261.677C771.255 266.432 776.274 268.809 783.494 268.809H799.607C801.368 268.809 802.776 269.425 803.833 270.658C805.066 271.715 805.682 273.123 805.682 274.884V284.393C805.682 286.154 805.066 287.651 803.833 288.884C802.776 289.94 801.368 290.469 799.607 290.469H781.381Z" fill="#2D2E2F"/>
|
||||
<path d="M834.374 290.469C832.613 290.469 831.117 289.94 829.884 288.884C828.827 287.651 828.299 286.154 828.299 284.393V159.19C828.299 157.429 828.827 156.02 829.884 154.964C831.117 153.731 832.613 153.115 834.374 153.115H847.053C848.814 153.115 850.223 153.731 851.28 154.964C852.336 156.02 852.864 157.429 852.864 159.19V284.393C852.864 286.154 852.336 287.651 851.28 288.884C850.223 289.94 848.814 290.469 847.053 290.469H834.374ZM832.261 127.493C830.5 127.493 829.004 126.965 827.771 125.908C826.714 124.675 826.186 123.179 826.186 121.418V107.154C826.186 105.393 826.714 103.984 827.771 102.928C829.004 101.695 830.5 101.079 832.261 101.079H848.902C850.663 101.079 852.072 101.695 853.129 102.928C854.361 103.984 854.978 105.393 854.978 107.154V121.418C854.978 123.179 854.361 124.675 853.129 125.908C852.072 126.965 850.663 127.493 848.902 127.493H832.261Z" fill="#2D2E2F"/>
|
||||
<path d="M912.082 290.469C910.321 290.469 908.824 289.94 907.592 288.884C906.535 287.651 906.007 286.154 906.007 284.393V174.246H883.555C881.794 174.246 880.297 173.718 879.064 172.661C878.008 171.429 877.479 169.932 877.479 168.171V159.19C877.479 157.429 878.008 156.02 879.064 154.964C880.297 153.731 881.794 153.115 883.555 153.115H906.007V139.908C906.007 130.927 907.504 123.091 910.497 116.399C913.491 109.531 918.245 104.249 924.761 100.551C931.453 96.8525 940.345 95.0035 951.439 95.0035H967.288C969.049 95.0035 970.458 95.6199 971.514 96.8525C972.571 97.9091 973.099 99.3179 973.099 101.079V110.06C973.099 111.821 972.571 113.317 971.514 114.55C970.458 115.607 969.049 116.135 967.288 116.135H951.968C943.691 116.135 938.056 118.336 935.062 122.738C932.069 126.965 930.572 133.128 930.572 141.228V153.115H964.646C966.407 153.115 967.816 153.731 968.873 154.964C969.929 156.02 970.458 157.429 970.458 159.19V168.171C970.458 169.932 969.929 171.429 968.873 172.661C967.816 173.718 966.407 174.246 964.646 174.246H930.572V284.393C930.572 286.154 929.956 287.651 928.723 288.884C927.667 289.94 926.258 290.469 924.497 290.469H912.082Z" fill="#2D2E2F"/>
|
||||
<path d="M1017.94 340.656C1016.53 340.656 1015.3 340.127 1014.24 339.071C1013.19 338.014 1012.66 336.782 1012.66 335.373C1012.66 334.668 1012.75 333.964 1012.92 333.26C1013.1 332.555 1013.45 331.675 1013.98 330.618L1034.32 282.28L983.867 163.152C982.987 161.039 982.546 159.542 982.546 158.662C982.546 157.077 983.075 155.756 984.131 154.7C985.188 153.643 986.509 153.115 988.093 153.115H1001.04C1002.8 153.115 1004.21 153.555 1005.26 154.435C1006.32 155.316 1007.02 156.373 1007.38 157.605L1047.53 253.753L1088.73 157.605C1089.26 156.373 1089.96 155.316 1090.84 154.435C1091.9 153.555 1093.4 153.115 1095.34 153.115H1107.75C1109.33 153.115 1110.66 153.643 1111.71 154.7C1112.77 155.756 1113.3 156.989 1113.3 158.398C1113.3 159.278 1112.86 160.863 1111.98 163.152L1036.96 336.165C1036.43 337.398 1035.64 338.454 1034.58 339.335C1033.7 340.215 1032.29 340.656 1030.36 340.656H1017.94Z" fill="#2D2E2F"/>
|
||||
<path d="M1128.65 290.469C1126.89 290.469 1125.4 289.94 1124.16 288.884C1123.11 287.651 1122.58 286.154 1122.58 284.393V265.375C1122.58 263.614 1123.11 262.205 1124.16 261.149C1125.4 259.916 1126.89 259.3 1128.65 259.3H1147.67C1149.43 259.3 1150.84 259.916 1151.9 261.149C1153.13 262.205 1153.75 263.614 1153.75 265.375V284.393C1153.75 286.154 1153.13 287.651 1151.9 288.884C1150.84 289.94 1149.43 290.469 1147.67 290.469H1128.65Z" fill="#2D2E2F"/>
|
||||
<path d="M1244.66 348.58C1232.69 348.58 1222.83 346.995 1215.08 343.825C1207.51 340.656 1201.52 336.782 1197.12 332.203C1192.89 327.801 1189.81 323.31 1187.87 318.732C1186.11 314.329 1185.14 310.808 1184.97 308.166C1184.79 306.405 1185.32 304.908 1186.55 303.676C1187.96 302.443 1189.46 301.827 1191.04 301.827H1203.72C1205.31 301.827 1206.63 302.179 1207.68 302.883C1208.74 303.588 1209.62 305.084 1210.32 307.374C1211.38 310.015 1212.97 312.921 1215.08 316.09C1217.19 319.436 1220.45 322.254 1224.85 324.543C1229.25 327.008 1235.42 328.241 1243.34 328.241C1251.62 328.241 1258.57 327.096 1264.21 324.807C1269.84 322.694 1274.07 318.82 1276.89 313.185C1279.88 307.726 1281.38 300.154 1281.38 290.469V272.507C1277.15 277.966 1271.61 282.544 1264.74 286.242C1257.87 289.764 1249.24 291.525 1238.85 291.525C1228.99 291.525 1220.54 289.852 1213.49 286.507C1206.45 282.985 1200.64 278.318 1196.06 272.507C1191.66 266.52 1188.31 259.74 1186.02 252.168C1183.91 244.596 1182.68 236.672 1182.33 228.395C1182.15 223.465 1182.15 218.622 1182.33 213.867C1182.68 205.591 1183.91 197.667 1186.02 190.095C1188.31 182.523 1191.66 175.743 1196.06 169.756C1200.64 163.769 1206.45 159.102 1213.49 155.756C1220.54 152.234 1228.99 150.473 1238.85 150.473C1249.42 150.473 1258.13 152.498 1265 156.549C1272.05 160.423 1277.77 165.177 1282.17 170.812V159.454C1282.17 157.693 1282.7 156.196 1283.76 154.964C1284.99 153.731 1286.49 153.115 1288.25 153.115H1300.4C1302.16 153.115 1303.65 153.731 1304.89 154.964C1306.12 156.196 1306.74 157.693 1306.74 159.454V287.563C1306.74 299.361 1304.71 309.839 1300.66 318.996C1296.79 328.153 1290.27 335.373 1281.11 340.656C1272.13 345.938 1259.98 348.58 1244.66 348.58ZM1244.13 270.394C1252.94 270.394 1259.98 268.369 1265.27 264.319C1270.73 260.268 1274.69 255.162 1277.15 248.998C1279.79 242.835 1281.2 236.584 1281.38 230.244C1281.55 227.779 1281.64 224.785 1281.64 221.263C1281.64 217.565 1281.55 214.484 1281.38 212.018C1281.2 205.679 1279.79 199.428 1277.15 193.264C1274.69 187.101 1270.73 181.994 1265.27 177.944C1259.98 173.894 1252.94 171.869 1244.13 171.869C1235.33 171.869 1228.29 173.894 1223 177.944C1217.72 181.818 1213.93 187.013 1211.65 193.529C1209.36 199.868 1208.04 206.736 1207.68 214.132C1207.51 218.71 1207.51 223.377 1207.68 228.131C1208.04 235.527 1209.36 242.483 1211.65 248.998C1213.93 255.338 1217.72 260.533 1223 264.583C1228.29 268.457 1235.33 270.394 1244.13 270.394Z" fill="#2D2E2F"/>
|
||||
<path d="M1394.35 293.11C1380.96 293.11 1369.78 290.557 1360.8 285.45C1351.82 280.343 1344.95 273.299 1340.2 264.319C1335.44 255.162 1332.8 244.772 1332.27 233.15C1332.1 230.156 1332.01 226.37 1332.01 221.792C1332.01 217.037 1332.1 213.251 1332.27 210.434C1332.8 198.635 1335.44 188.246 1340.2 179.265C1345.13 170.284 1352.08 163.24 1361.07 158.133C1370.05 153.027 1381.14 150.473 1394.35 150.473C1407.55 150.473 1418.65 153.027 1427.63 158.133C1436.61 163.24 1443.48 170.284 1448.23 179.265C1453.16 188.246 1455.89 198.635 1456.42 210.434C1456.6 213.251 1456.69 217.037 1456.69 221.792C1456.69 226.37 1456.6 230.156 1456.42 233.15C1455.89 244.772 1453.25 255.162 1448.5 264.319C1443.74 273.299 1436.87 280.343 1427.89 285.45C1418.91 290.557 1407.73 293.11 1394.35 293.11ZM1394.35 272.771C1405.27 272.771 1413.98 269.337 1420.5 262.47C1427.01 255.426 1430.53 245.212 1431.06 231.829C1431.24 229.188 1431.33 225.842 1431.33 221.792C1431.33 217.742 1431.24 214.396 1431.06 211.754C1430.53 198.371 1427.01 188.246 1420.5 181.378C1413.98 174.334 1405.27 170.812 1394.35 170.812C1383.43 170.812 1374.62 174.334 1367.93 181.378C1361.42 188.246 1357.98 198.371 1357.63 211.754C1357.46 214.396 1357.37 217.742 1357.37 221.792C1357.37 225.842 1357.46 229.188 1357.63 231.829C1357.98 245.212 1361.42 255.426 1367.93 262.47C1374.62 269.337 1383.43 272.771 1394.35 272.771Z" fill="#2D2E2F"/>
|
||||
<path d="M1524.75 290.469C1522.1 290.469 1519.99 289.852 1518.41 288.62C1517 287.387 1515.85 285.802 1514.97 283.865L1466.11 161.303C1465.75 160.423 1465.58 159.542 1465.58 158.662C1465.58 157.077 1466.11 155.756 1467.16 154.7C1468.22 153.643 1469.54 153.115 1471.12 153.115H1483.8C1485.74 153.115 1487.24 153.643 1488.29 154.7C1489.35 155.756 1489.97 156.725 1490.14 157.605L1530.56 261.677L1570.71 157.605C1571.06 156.725 1571.67 155.756 1572.56 154.7C1573.61 153.643 1575.11 153.115 1577.05 153.115H1589.99C1591.4 153.115 1592.63 153.643 1593.69 154.7C1594.92 155.756 1595.54 157.077 1595.54 158.662C1595.54 159.542 1595.36 160.423 1595.01 161.303L1545.88 283.865C1545.17 285.802 1544.03 287.387 1542.44 288.62C1541.03 289.852 1538.92 290.469 1536.1 290.469H1524.75Z" fill="#2D2E2F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
BIN
app/assets/images/notify-og-image.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
app/assets/images/product/security-review-message.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
24
app/assets/images/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="260.000000pt" height="260.000000pt" viewBox="0 0 260.000000 260.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,260.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1177 2586 c-67 -29 -117 -107 -117 -183 0 -12 -15 -24 -48 -37 -175
|
||||
-69 -342 -229 -426 -406 -74 -156 -77 -183 -78 -657 l-1 -423 -123 -116 -124
|
||||
-117 0 -109 0 -108 996 2 997 3 -1 105 -1 105 -123 117 -123 118 -1 310 c0
|
||||
170 -3 313 -7 316 -4 4 -26 8 -50 9 -105 6 -216 59 -292 140 -152 161 -161
|
||||
417 -22 583 35 42 31 48 -71 97 -42 20 -84 41 -92 45 -8 4 -18 28 -22 54 -14
|
||||
100 -80 158 -182 162 -34 1 -74 -4 -89 -10z"/>
|
||||
<path d="M1854 2297 c-64 -28 -95 -48 -134 -89 -65 -68 -99 -163 -94 -265 11
|
||||
-271 317 -431 547 -286 163 103 215 311 122 482 -35 64 -91 114 -165 149 -77
|
||||
37 -202 41 -276 9z"/>
|
||||
<path d="M910 282 c0 -32 32 -116 59 -154 114 -161 427 -170 566 -16 42 47 75
|
||||
119 75 165 l0 23 -350 0 c-332 0 -350 -1 -350 -18z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1,19 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
return $(`<div class="selection-footer${stickyClass}">
|
||||
<button
|
||||
class="govuk-button govuk-button--secondary selection-footer__button"
|
||||
class="govuk-button govuk-button--secondary selection-footer__button usa-button usa-button--outline"
|
||||
aria-expanded="${expanded ? 'true' : 'false'}"
|
||||
aria-controls="${this.fieldsetId}">
|
||||
${buttonContent}
|
||||
|
||||
41
app/assets/javascripts/loginAlert.js
Normal file
@@ -0,0 +1,41 @@
|
||||
(function (window) {
|
||||
|
||||
// Set the target date (10 days before March 15th, 2024)
|
||||
const targetDate = new Date("April 16, 2024 00:00:00").getTime();
|
||||
|
||||
// Set a const for the container to write a conditional statement and only run if it exists
|
||||
const countdownContainer = document.getElementById("countdown-container");
|
||||
|
||||
// Function to update the countdown display
|
||||
function updateCountdown() {
|
||||
|
||||
if (countdownContainer) {
|
||||
const now = new Date().getTime();
|
||||
const difference = targetDate - now;
|
||||
|
||||
// Time calculations for days only
|
||||
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Visibility logic
|
||||
if (days < 0 || days > 10) {
|
||||
// Hide if more than 10 days away OR if already past the date
|
||||
countdownContainer.style.display = "none";
|
||||
} else {
|
||||
// Show if 10 days or less remaining
|
||||
countdownContainer.style.display = "block";
|
||||
document.getElementById("countdown").innerHTML = days + " days ";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expose the updateCountdown function to the outside world
|
||||
window.updateCountdown = updateCountdown;
|
||||
|
||||
// Initial display update
|
||||
updateCountdown();
|
||||
|
||||
// Update the countdown every second (inside the IIFE)
|
||||
setInterval(updateCountdown, 1000);
|
||||
|
||||
})(window);
|
||||
@@ -48,13 +48,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sms-message-sender {
|
||||
margin: units(1) 0 0;
|
||||
.sms-message-sender, .sms-message-file-name, .sms-message-scheduler, .sms-message-template, .sms-message-sender {
|
||||
margin:0.25rem 0 0;
|
||||
}
|
||||
|
||||
.sms-message-recipient {
|
||||
color: color('gray-cool-90');
|
||||
margin: 0 0 units(1);
|
||||
margin: units(1) 0 units(1);
|
||||
}
|
||||
|
||||
.sms-message-status {
|
||||
@@ -62,6 +62,14 @@
|
||||
margin: -20px units(1) 20px units(1);
|
||||
}
|
||||
|
||||
h2.sms-message-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h2.recipient-list {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sms-message-status-outbound {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -131,7 +139,7 @@
|
||||
&-label,
|
||||
&-button-label {
|
||||
font-weight: bold;
|
||||
font-size: 19px;
|
||||
font-size: 19px;
|
||||
display: block;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
@@ -22,27 +22,46 @@ i.e.
|
||||
|
||||
@use "uswds-core" as *;
|
||||
|
||||
iframe:focus, [href]:focus, [tabindex]:focus, [contentEditable=true]:focus {
|
||||
outline: units(1px) dotted color('blue-40v');
|
||||
outline: units(1px) auto color('blue-40v');
|
||||
outline-offset: 0.3rem;
|
||||
}
|
||||
|
||||
.usa-header--extended {
|
||||
.usa-logo {
|
||||
font-family: family("sans");
|
||||
margin: units(4) 0;
|
||||
@include at-media-max('mobile-lg') {
|
||||
margin: units(3) 0;
|
||||
@include at-media-max('desktop') {
|
||||
margin: units(4) 0 units(4) units(2);
|
||||
}
|
||||
img {
|
||||
@include at-media($theme-header-min-width) {
|
||||
width: 80px;
|
||||
height: 70px;
|
||||
a {
|
||||
display: flex;
|
||||
img {
|
||||
height: 35px;
|
||||
@include at-media('desktop') {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.usa-nav__secondary {
|
||||
.usa-nav__link {
|
||||
padding: 0;
|
||||
&.usa-current {
|
||||
text-decoration: underline;
|
||||
}
|
||||
padding: 0;
|
||||
&.usa-current {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@include at-media-max('desktop') {
|
||||
padding: 0 units(2);
|
||||
ul li {
|
||||
padding-bottom: units(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.usa-nav-container {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,18 +71,23 @@ i.e.
|
||||
|
||||
.usa-nav__primary {
|
||||
&> .usa-nav__primary-item > a {
|
||||
font-size: size("body", 4);
|
||||
font-size: size("body", 5);
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
}
|
||||
// &> .usa-nav__primary-item:last-child {
|
||||
// margin-left: auto;
|
||||
// @include u-margin-right(-4);
|
||||
// }
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.usa-section--dark h1 {
|
||||
color: color('blue-30v');
|
||||
@include at-media-max('desktop') {
|
||||
font-size: size("body", 12);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
.list li {
|
||||
font-family: family("sans");
|
||||
@@ -124,6 +148,7 @@ td.table-empty-message {
|
||||
.button-flex-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: units(2);
|
||||
@include at-media(desktop) {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
@@ -139,27 +164,37 @@ td.table-empty-message {
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: units(2);
|
||||
.user-list-item {
|
||||
@include at-media(desktop) {
|
||||
flex: 1 1 calc(50% - units(2));
|
||||
}
|
||||
flex: 1 1 calc(100% - units(2));
|
||||
border: 1px solid color('gray-cool-10');
|
||||
padding: units(2);
|
||||
.tick-cross-list-permissions {
|
||||
margin: units(1) 0;
|
||||
padding-left: units(2);
|
||||
}
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: size("body", "sm");
|
||||
font-weight: normal;
|
||||
}
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.user-list-item {
|
||||
@include at-media(desktop) {
|
||||
width: calc(50% - units(1));
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.user-list-item-heading .live-search-relevant {
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
border: 1px solid color('gray-cool-10');
|
||||
padding: units(2);
|
||||
|
||||
.tick-cross-list-permissions {
|
||||
margin: units(1) 0;
|
||||
padding-left: units(2);
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: size("body", "sm");
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.template-list-item-without-ancestors .template-list-folder:active,
|
||||
.template-list-item-without-ancestors .template-list-folder:active::before,
|
||||
@@ -284,27 +319,15 @@ td.table-empty-message {
|
||||
|
||||
.dashboard {
|
||||
.big-number-with-status {
|
||||
a {
|
||||
background: color("blue-60v");
|
||||
padding: units(2);
|
||||
margin-bottom: units(1);
|
||||
text-decoration: none;
|
||||
&:hover{
|
||||
background: color("blue-warm-70v");
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
.big-number-smaller {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.big-number-number {
|
||||
font-size: units(5);
|
||||
line-height: units(6);
|
||||
line-height: units(5);
|
||||
}
|
||||
.big-number-label {
|
||||
font-size: units(3);
|
||||
font-size: units(2.5);
|
||||
}
|
||||
}
|
||||
.big-number-status {
|
||||
@@ -341,6 +364,9 @@ td.table-empty-message {
|
||||
background-image: url(../img/material-icons/description.svg);
|
||||
}
|
||||
}
|
||||
.table-wrapper {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
@@ -352,6 +378,7 @@ td.table-empty-message {
|
||||
}
|
||||
.file-list-hint {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
.table-field, .table-field-right-aligned {
|
||||
width: 50%;
|
||||
@@ -375,6 +402,36 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.job-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
td.file-name {
|
||||
width: 25%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
td.template {
|
||||
width: 20%;
|
||||
}
|
||||
td.time-sent {
|
||||
width: 20%;
|
||||
}
|
||||
td.sender {
|
||||
width: 15%;
|
||||
}
|
||||
td.count-of-recipients {
|
||||
width: 5%;
|
||||
}
|
||||
td.report {
|
||||
width: 5%;
|
||||
}
|
||||
th {
|
||||
padding: 0.5rem 1rem
|
||||
}
|
||||
td {
|
||||
padding: 0.5rem 1rem
|
||||
}
|
||||
}
|
||||
|
||||
#template-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
@@ -520,3 +577,15 @@ details form {
|
||||
.edit-textbox-error-mt {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
// Login page
|
||||
|
||||
#countdown-container {
|
||||
display: none; // Hide the countdown timer
|
||||
}
|
||||
|
||||
.placeholder, .placeholder-conditional {
|
||||
background-color: #face00;
|
||||
border: 1px solid #face00;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
@@ -9,5 +9,9 @@ in the form $setting: value,
|
||||
|
||||
@use "uswds-core" with (
|
||||
$theme-font-type-sans: "public-sans",
|
||||
$theme-show-notifications: false
|
||||
$theme-show-notifications: false,
|
||||
$theme-banner-max-width: "desktop-lg",
|
||||
$theme-grid-container-max-width: "desktop-lg",
|
||||
$theme-footer-max-width: "desktop-lg",
|
||||
$theme-header-max-width: "desktop-lg"
|
||||
);
|
||||
|
||||
@@ -36,11 +36,7 @@ class Config(object):
|
||||
NR_BROWSER_KEY = getenv("NR_BROWSER_KEY")
|
||||
settings = newrelic.agent.global_settings()
|
||||
NR_MONITOR_ON = settings and settings.monitor_mode
|
||||
|
||||
TEMPLATE_PREVIEW_API_HOST = getenv(
|
||||
"TEMPLATE_PREVIEW_API_HOST", "http://localhost:9999"
|
||||
)
|
||||
TEMPLATE_PREVIEW_API_KEY = getenv("TEMPLATE_PREVIEW_API_KEY", "my-secret-key")
|
||||
COMMIT_HASH = getenv("COMMIT_HASH", "--------")[0:7]
|
||||
|
||||
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov"]
|
||||
|
||||
@@ -81,8 +77,7 @@ class Config(object):
|
||||
"IBAN": "GB33BUKB20201555555555",
|
||||
"swift": "ABCDEF12",
|
||||
"notify_billing_email_addresses": [
|
||||
"generic@digital.cabinet-office.gov.uk",
|
||||
"first.last@digital.cabinet-office.gov.uk",
|
||||
"tts-benefits-studio@gsa.gov",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -152,6 +147,34 @@ class Staging(Production):
|
||||
HEADER_COLOUR = "#00ff00" # $green
|
||||
|
||||
|
||||
class E2ETest(Staging):
|
||||
"""
|
||||
An environment config that is intended to operate as if it were in the
|
||||
staging environment but with the configuration of the development and test
|
||||
environments so the E2E tests work.
|
||||
"""
|
||||
|
||||
# Borrowed from development environment
|
||||
DEBUG = True
|
||||
SESSION_COOKIE_SECURE = False
|
||||
SESSION_PROTECTION = None
|
||||
HTTP_PROTOCOL = "http"
|
||||
ASSET_DOMAIN = ""
|
||||
ASSET_PATH = "/static/"
|
||||
|
||||
# Borrowed from test environment
|
||||
TESTING = True
|
||||
WTF_CSRF_ENABLED = False
|
||||
|
||||
# buckets - mirror staging
|
||||
CSV_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
||||
"notify-api-csv-upload-bucket-staging"
|
||||
)
|
||||
LOGO_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
||||
"notify-admin-logo-upload-bucket-staging"
|
||||
)
|
||||
|
||||
|
||||
class Demo(Staging):
|
||||
HEADER_COLOUR = "#6F72AF" # $mauve
|
||||
|
||||
@@ -173,6 +196,7 @@ class Scanning(Production):
|
||||
configs = {
|
||||
"development": Development,
|
||||
"test": Test,
|
||||
"e2etest": E2ETest,
|
||||
"scanning": Scanning,
|
||||
"staging": Staging,
|
||||
"demo": Demo,
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
# Get started
|
||||
|
||||
|
||||
1. ## Check if Notify.gov is right for you
|
||||
<p>Read about our <a class="usa-link" href="{{ url_for('main.features') }}">features</a>, <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a> and <a class="usa-link" href="{{ url_for('main.roadmap') }}">roadmap</a>.</p>
|
||||
1. ## Everyone starts in [Trial Mode](/using-notify/trial-mode)
|
||||
Explore Notify, add team members, and practice [sending messages to teammates](/using-notify/trial-mode).
|
||||
|
||||
2. ## Personalize content
|
||||
Learn how to [personalize messages](/using-notify/guidance) to increase response.
|
||||
|
||||
2. ## Create an account
|
||||
{% if not current_user.is_authenticated %}
|
||||
<p><a class="usa-link" href="{{ url_for('.register') }}">Create an account</a> for free and add your first Notify service. When you add a new service it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% else %}
|
||||
<p>Create an account for free and add your first Notify service. When you add a new service, it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% endif %}
|
||||
|
||||
3. ## Write some messages
|
||||
{% if True %}
|
||||
<p>Add message templates with examples of the content you plan to send. You can use our <a class="usa-link" href="{{ url_for('main.guidance_index') }}">guidance</a> to help you.</p>
|
||||
{% endif %}
|
||||
|
||||
4. ## Set up your service
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>Review your settings to add message branding and sender information.</p>
|
||||
<p>Add team members and check their permissions.</p>
|
||||
{% else %}
|
||||
<p>Review your <a class="usa-link" href="{{ url_for('.service_settings', service_id=current_service.id) }}">settings</a> to add message branding and sender information.</p>
|
||||
<p>Add <a class="usa-link" href="{{ url_for('.manage_users', service_id=current_service.id) }}">team members</a> and check their permissions.</p>
|
||||
{% endif %}
|
||||
3. ## Check delivery status
|
||||
[Analyze the delivery](/using-notify/delivery-status) of your messages and download reports
|
||||
|
||||
4. ## View messaging limits
|
||||
Learn about message _parts_ and [how limits are calculated](/using-notify/pricing).
|
||||
|
||||
5. ## Start sending messages
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>When you’re ready to send messages to people outside your team, go to the <b class="bold">Settings</b> page and select <b class="bold">Request to go live</b>. We’ll approve your request within one working day.</p>
|
||||
{% else %}
|
||||
<p>You should <a class="usa-link" href="{{ url_for('.support') }}">request to go live</a> when you’re ready to send messages to people outside your team. We’ll approve your request within one working day.</p>
|
||||
{% endif %}
|
||||
To remove the restrictions of Trial Mode and begin sending messages to clients complete the [Live Campaign Form](https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL).
|
||||
We’ll respond within one business day.
|
||||
|
||||
<!-- <li class="get-started-list__item">
|
||||
<h2 class="usa-process-list__heading line-height-sans-1">Set up an API integration (optional)</h2>
|
||||
<p>You can use the Notify API to send messages automatically.</p>
|
||||
<p>Our <a class="usa-link" href="{{ url_for('main.documentation') }}">documentation</a> explains how to integrate the API with a web application or back office system.</p>
|
||||
</li> -->
|
||||
|
||||
<!-- <p>Check <a class="usa-link" href="{{ url_for('main.how_to_pay') }}">how to pay</a> if you’re planning to exceed the <a class="usa-link" href="{{ url_for('.pricing', _anchor='text-messages') }}">free text message allowance</a>.</p> -->
|
||||
### Questions?
|
||||
[Contact the Notify team](/support)
|
||||
|
||||
@@ -22,7 +22,7 @@ from notifications_utils.recipients import InvalidPhoneError, validate_phone_num
|
||||
from notifications_utils.take import Take
|
||||
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
from app.utils.time import parse_naive_dt
|
||||
from app.utils.time import parse_dt, parse_naive_dt
|
||||
|
||||
|
||||
def apply_html_class(tags, html_file):
|
||||
@@ -87,15 +87,22 @@ def format_time(date):
|
||||
|
||||
|
||||
def format_datetime_normal(date):
|
||||
# example: February 20, 2024 at 07:00 PM US/Eastern, used for datetimes that's not within tables
|
||||
return "{} at {} {}".format(
|
||||
format_date_normal(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
format_date_normal(date), format_time_12h(date), get_user_preferred_timezone()
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_short(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date_short(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
def format_datetime_table(date):
|
||||
# example: 03-18-2024 at 04:53 PM, intended for datetimes in tables
|
||||
return "{} at {}".format(format_date_numeric(date), format_time_12h(date))
|
||||
|
||||
|
||||
def format_time_12h(date):
|
||||
date = parse_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return date.astimezone(preferred_tz).strftime("%I:%M %p")
|
||||
|
||||
|
||||
def format_datetime_relative(date):
|
||||
@@ -115,7 +122,7 @@ def format_date_numeric(date):
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%Y-%m-%d")
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
|
||||
)
|
||||
|
||||
|
||||
@@ -164,7 +171,7 @@ def format_date(date):
|
||||
|
||||
def format_date_normal(date):
|
||||
date = parse_naive_dt(date)
|
||||
return date.strftime("%d %B %Y").lstrip("0")
|
||||
return date.strftime("%B %d, %Y").lstrip("0")
|
||||
|
||||
|
||||
def format_date_short(date):
|
||||
@@ -448,6 +455,12 @@ def message_count_noun(count, template_type):
|
||||
else:
|
||||
return "text messages"
|
||||
|
||||
if template_type == "parts":
|
||||
if count == 1:
|
||||
return "text message part"
|
||||
else:
|
||||
return "text message parts"
|
||||
|
||||
elif template_type == "email":
|
||||
if count == 1:
|
||||
return "email"
|
||||
|
||||
@@ -70,7 +70,7 @@ from app.utils.user_permissions import all_ui_permissions, permission_options
|
||||
def get_time_value_and_label(future_time):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
future_time.astimezone(preferred_tz).replace(tzinfo=None).isoformat(),
|
||||
future_time.astimezone(preferred_tz).isoformat(),
|
||||
"{} at {} {}".format(
|
||||
get_human_day(future_time.astimezone(preferred_tz)),
|
||||
get_human_time(future_time.astimezone(preferred_tz)),
|
||||
@@ -605,6 +605,15 @@ class RegisterUserForm(StripWhitespaceForm):
|
||||
auth_type = HiddenField("auth_type", default="sms_auth")
|
||||
|
||||
|
||||
class SetupUserProfileForm(StripWhitespaceForm):
|
||||
name = GovukTextInputField(
|
||||
"Full name", validators=[DataRequired(message="Cannot be empty")]
|
||||
)
|
||||
mobile_number = international_phone_number()
|
||||
# TODO This should be replaced with a select widget when one is available.
|
||||
preferred_timezone = HiddenField("preferred_timezone", default="US/Eastern")
|
||||
|
||||
|
||||
class RegisterUserFromInviteForm(RegisterUserForm):
|
||||
def __init__(self, invited_user):
|
||||
super().__init__(
|
||||
@@ -1226,7 +1235,11 @@ class CsvUploadForm(StripWhitespaceForm):
|
||||
validators=[
|
||||
DataRequired(message="Please pick a file"),
|
||||
CsvFileValidator(),
|
||||
FileSize(max_size=10e6, message="File must be smaller than 10Mb"), # 10Mb
|
||||
FileSize(
|
||||
max_size=10e6,
|
||||
message="File must be smaller than 10Mb. If you are trying to upload an Excel file, \
|
||||
please export the contents in the CSV format and then try again.",
|
||||
), # 10Mb
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1253,7 +1266,6 @@ class ChangePreferredTimezoneForm(StripWhitespaceForm):
|
||||
|
||||
new_preferred_timezone = GovukRadiosField(
|
||||
"What timezone would you like to use?",
|
||||
default="US/Eastern",
|
||||
)
|
||||
|
||||
|
||||
@@ -1782,12 +1794,16 @@ class TemplateAndFoldersSelectionForm(Form):
|
||||
None,
|
||||
[
|
||||
# ('email', 'Email') if 'email' in available_template_types else None,
|
||||
("sms", "Start with a blank template")
|
||||
if "sms" in available_template_types
|
||||
else None,
|
||||
("copy-existing", "Copy an existing template")
|
||||
if allow_adding_copy_of_template
|
||||
else None,
|
||||
(
|
||||
("sms", "Start with a blank template")
|
||||
if "sms" in available_template_types
|
||||
else None
|
||||
),
|
||||
(
|
||||
("copy-existing", "Copy an existing template")
|
||||
if allow_adding_copy_of_template
|
||||
else None
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
@@ -168,12 +168,14 @@ def api_callbacks(service_id):
|
||||
|
||||
return render_template(
|
||||
"views/api/callbacks.html",
|
||||
received_text_messages_callback=received_text_messages_callback["url"]
|
||||
if received_text_messages_callback
|
||||
else None,
|
||||
delivery_status_callback=delivery_status_callback["url"]
|
||||
if delivery_status_callback
|
||||
else None,
|
||||
received_text_messages_callback=(
|
||||
received_text_messages_callback["url"]
|
||||
if received_text_messages_callback
|
||||
else None
|
||||
),
|
||||
delivery_status_callback=(
|
||||
delivery_status_callback["url"] if delivery_status_callback else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -262,9 +264,11 @@ def received_text_messages_callback(service_id):
|
||||
|
||||
received_text_messages_callback = get_received_text_messages_callback()
|
||||
form = CallbackForm(
|
||||
url=received_text_messages_callback.get("url")
|
||||
if received_text_messages_callback
|
||||
else "",
|
||||
url=(
|
||||
received_text_messages_callback.get("url")
|
||||
if received_text_messages_callback
|
||||
else ""
|
||||
),
|
||||
bearer_token=dummy_bearer_token if received_text_messages_callback else "",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import calendar
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
@@ -47,54 +48,41 @@ def service_dashboard(service_id):
|
||||
if not current_user.has_permissions("view_activity"):
|
||||
return redirect(url_for("main.choose_template", service_id=service_id))
|
||||
|
||||
download_availability = []
|
||||
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
service_id=service_id,
|
||||
job_response = job_api_client.get_jobs(service_id)["data"]
|
||||
notifications_response = notification_api_client.get_notifications_for_service(
|
||||
service_id
|
||||
)["notifications"]
|
||||
|
||||
notificaton_job_ids = [
|
||||
notification["job"]["id"]
|
||||
for notification in notifications
|
||||
if "job" in notification
|
||||
]
|
||||
|
||||
jobs = []
|
||||
|
||||
for notificaton_job_id in notificaton_job_ids:
|
||||
job_data = job_api_client.get_job(service_id, notificaton_job_id)["data"]
|
||||
if job_data:
|
||||
jobs.append(job_data)
|
||||
|
||||
service_data_retention_days = 7
|
||||
download_availability = []
|
||||
for job in jobs:
|
||||
message_type = job.get("template_type")
|
||||
if message_type is not None:
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
message_type
|
||||
)
|
||||
time_left = get_time_left(job["created_at"])
|
||||
download_link = (
|
||||
url_for(
|
||||
".view_job_csv",
|
||||
service_id=current_service.id,
|
||||
job_id=job["id"],
|
||||
|
||||
aggregate_notifications_by_job = defaultdict(list)
|
||||
for notification in notifications_response:
|
||||
job_id = notification.get("job", {}).get("id", None)
|
||||
if job_id:
|
||||
aggregate_notifications_by_job[job_id].append(notification)
|
||||
|
||||
job_and_notifications = [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
"download_link": url_for(
|
||||
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
)
|
||||
download_availability.append(
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": time_left,
|
||||
"download_link": download_link,
|
||||
}
|
||||
)
|
||||
"view_job_link": url_for(
|
||||
".view_job", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"created_at": job["created_at"],
|
||||
"notification_count": job["notification_count"],
|
||||
"created_by": job["created_by"],
|
||||
"notifications": aggregate_notifications_by_job.get(job["id"], []),
|
||||
}
|
||||
for job in job_response
|
||||
if aggregate_notifications_by_job.get(job["id"], [])
|
||||
]
|
||||
return render_template(
|
||||
"views/dashboard/dashboard.html",
|
||||
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||
partials=get_dashboard_partials(service_id),
|
||||
notifications=notifications,
|
||||
download_availability=download_availability,
|
||||
job_and_notifications=job_and_notifications,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from flask import abort, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
@@ -8,6 +10,8 @@ from app.main.views.pricing import CURRENT_SMS_RATE
|
||||
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
|
||||
from app.utils.user import user_is_logged_in
|
||||
|
||||
login_dot_gov_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
|
||||
|
||||
@main.route("/")
|
||||
def index():
|
||||
@@ -18,6 +22,7 @@ def index():
|
||||
"views/signedout.html",
|
||||
sms_rate=CURRENT_SMS_RATE,
|
||||
counts=status_api_client.get_count_of_live_services_and_organizations(),
|
||||
login_dot_gov_url=login_dot_gov_url,
|
||||
)
|
||||
|
||||
|
||||
@@ -102,15 +107,6 @@ def security():
|
||||
return render_template("views/security.html", navigation_links=features_nav())
|
||||
|
||||
|
||||
@main.route("/features/terms", endpoint="terms")
|
||||
@user_is_logged_in
|
||||
def terms():
|
||||
return render_template(
|
||||
"views/terms-of-use.html",
|
||||
navigation_links=features_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/features/using_notify")
|
||||
@user_is_logged_in
|
||||
def using_notify():
|
||||
@@ -214,7 +210,6 @@ def send_files_by_email():
|
||||
|
||||
|
||||
@main.route("/roadmap", endpoint="old_roadmap")
|
||||
@main.route("/terms", endpoint="old_terms")
|
||||
@main.route("/information-security", endpoint="information_security")
|
||||
@main.route("/using_notify", endpoint="old_using_notify")
|
||||
@main.route("/information-risk-management", endpoint="information_risk_management")
|
||||
@@ -222,7 +217,6 @@ def send_files_by_email():
|
||||
def old_page_redirects():
|
||||
redirects = {
|
||||
"main.old_roadmap": "main.roadmap",
|
||||
"main.old_terms": "main.terms",
|
||||
"main.information_security": "main.using_notify",
|
||||
"main.old_using_notify": "main.using_notify",
|
||||
"main.information_risk_management": "main.security",
|
||||
|
||||
@@ -10,6 +10,7 @@ from flask import (
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
stream_with_context,
|
||||
url_for,
|
||||
)
|
||||
@@ -18,7 +19,7 @@ from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTem
|
||||
|
||||
from app import (
|
||||
current_service,
|
||||
format_datetime_short,
|
||||
format_datetime_table,
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
)
|
||||
@@ -93,7 +94,7 @@ def view_job_csv(service_id, job_id):
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": 'inline; filename="{} - {}.csv"'.format(
|
||||
job.template["name"], format_datetime_short(job.created_at)
|
||||
job.template["name"], format_datetime_table(job.created_at)
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -276,12 +277,16 @@ def get_status_filters(service, message_type, statistics):
|
||||
}
|
||||
else:
|
||||
stats = statistics[message_type]
|
||||
stats["sending"] = stats["requested"] - stats["delivered"] - stats["failed"]
|
||||
|
||||
if stats.get("failure") is not None:
|
||||
stats["failed"] = stats["failure"]
|
||||
|
||||
stats["pending"] = stats["requested"] - stats["delivered"] - stats["failed"]
|
||||
|
||||
filters = [
|
||||
# key, label, option
|
||||
("requested", "total", "sending,delivered,failed"),
|
||||
("sending", "pending", "pending"),
|
||||
("pending", "pending", "pending"),
|
||||
("delivered", "delivered", "delivered"),
|
||||
("failed", "failed", "failed"),
|
||||
]
|
||||
@@ -296,7 +301,7 @@ def get_status_filters(service, message_type, statistics):
|
||||
message_type=message_type,
|
||||
status=option,
|
||||
),
|
||||
stats[key],
|
||||
stats.get(key),
|
||||
)
|
||||
for key, label, option in filters
|
||||
]
|
||||
@@ -369,6 +374,13 @@ def get_job_partials(job):
|
||||
job.template_type
|
||||
)
|
||||
|
||||
if request.referrer is not None:
|
||||
session["arrived_from_preview_page"] = "check" in request.referrer
|
||||
else:
|
||||
session["arrived_from_preview_page"] = False
|
||||
|
||||
arrived_from_preview_page_url = session.get("arrived_from_preview_page", False)
|
||||
|
||||
return {
|
||||
"counts": counts,
|
||||
"notifications": render_template(
|
||||
@@ -392,6 +404,7 @@ def get_job_partials(job):
|
||||
"status": render_template(
|
||||
"partials/jobs/status.html",
|
||||
job=job,
|
||||
arrived_from_preview_page_url=arrived_from_preview_page_url,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -122,16 +122,18 @@ def edit_user_permissions(service_id, user_id):
|
||||
form = form_class.from_user(
|
||||
user,
|
||||
service_id,
|
||||
folder_permissions=None
|
||||
if user.platform_admin
|
||||
else [
|
||||
f["id"]
|
||||
for f in current_service.all_template_folders
|
||||
if user.has_template_folder_permission(f)
|
||||
],
|
||||
all_template_folders=None
|
||||
if user.platform_admin
|
||||
else current_service.all_template_folders,
|
||||
folder_permissions=(
|
||||
None
|
||||
if user.platform_admin
|
||||
else [
|
||||
f["id"]
|
||||
for f in current_service.all_template_folders
|
||||
if user.has_template_folder_permission(f)
|
||||
]
|
||||
),
|
||||
all_template_folders=(
|
||||
None if user.platform_admin else current_service.all_template_folders
|
||||
),
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
|
||||
@@ -11,12 +11,7 @@ from flask import (
|
||||
url_for,
|
||||
)
|
||||
|
||||
from app import (
|
||||
current_service,
|
||||
format_date_numeric,
|
||||
job_api_client,
|
||||
notification_api_client,
|
||||
)
|
||||
from app import current_service, job_api_client, notification_api_client
|
||||
from app.main import main
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_TEST
|
||||
from app.utils import (
|
||||
@@ -26,7 +21,7 @@ from app.utils import (
|
||||
parse_filter_args,
|
||||
set_status_filters,
|
||||
)
|
||||
from app.utils.csv import generate_notifications_csv
|
||||
from app.utils.csv import generate_notifications_csv, get_user_preferred_timezone
|
||||
from app.utils.templates import get_template
|
||||
from app.utils.user import user_has_permissions
|
||||
|
||||
@@ -95,7 +90,7 @@ def view_notification(service_id, notification_id, error_message=None):
|
||||
partials=get_single_notification_partials(notification),
|
||||
created_by=notification.get("created_by"),
|
||||
created_at=notification["created_at"],
|
||||
updated_at=notification["updated_at"],
|
||||
updated_at=notification["sent_at"],
|
||||
help=get_help_argument(),
|
||||
notification_id=notification["id"],
|
||||
can_receive_inbound=(current_service.has_permission("inbound_sms")),
|
||||
@@ -146,6 +141,9 @@ def download_notifications_csv(service_id):
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
filter_args.get("message_type")[0]
|
||||
)
|
||||
file_time = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
|
||||
file_time = f"{file_time} {get_user_preferred_timezone()}"
|
||||
|
||||
return Response(
|
||||
stream_with_context(
|
||||
generate_notifications_csv(
|
||||
@@ -162,7 +160,7 @@ def download_notifications_csv(service_id):
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
|
||||
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
|
||||
file_time,
|
||||
filter_args["message_type"][0],
|
||||
current_service.name,
|
||||
)
|
||||
|
||||
@@ -34,8 +34,8 @@ def performance():
|
||||
stats["average_percentage_under_10_seconds"] = mean(
|
||||
[row["percentage_under_10_seconds"] for row in stats["processing_time"]] or [0]
|
||||
)
|
||||
stats[
|
||||
"count_of_live_services_and_organizations"
|
||||
] = status_api_client.get_count_of_live_services_and_organizations()
|
||||
stats["count_of_live_services_and_organizations"] = (
|
||||
status_api_client.get_count_of_live_services_and_organizations()
|
||||
)
|
||||
|
||||
return render_template("views/performance.html", **stats)
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from flask import abort, redirect, render_template, session, url_for
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
|
||||
from app import user_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
RegisterUserForm,
|
||||
RegisterUserFromInviteForm,
|
||||
RegisterUserFromOrgInviteForm,
|
||||
SetupUserProfileForm,
|
||||
)
|
||||
from app.main.views import sign_in
|
||||
from app.main.views.verify import activate_user
|
||||
from app.models.user import InvitedOrgUser, InvitedUser, User
|
||||
from app.utils import hide_from_search_engines
|
||||
@@ -120,4 +132,47 @@ def _do_registration(form, send_sms=True, send_email=True, organization_id=None)
|
||||
def registration_continue():
|
||||
if not session.get("user_details"):
|
||||
return redirect(url_for(".show_accounts_or_dashboard"))
|
||||
return render_template("views/registration-continue.html")
|
||||
else:
|
||||
raise Exception("Unexpected routing in registration_continue")
|
||||
|
||||
|
||||
@main.route("/set-up-your-profile", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def set_up_your_profile():
|
||||
|
||||
form = SetupUserProfileForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
# start login.gov
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
login_gov_error = request.args.get("error")
|
||||
if code and state:
|
||||
access_token = sign_in._get_access_token(code, state)
|
||||
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
elif login_gov_error:
|
||||
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||
# end login.gov
|
||||
|
||||
# create the user
|
||||
# TODO we have to provide something for password until that column goes away
|
||||
# TODO ideally we would set the user's preferred timezone here as well
|
||||
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
if user is None:
|
||||
user = User.register(
|
||||
name=form.name.data,
|
||||
email_address=user_email,
|
||||
mobile_number=form.mobile_number.data,
|
||||
password=str(uuid.uuid4()),
|
||||
auth_type="sms_auth",
|
||||
)
|
||||
|
||||
# activate the user
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
activate_user(user["id"])
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
return render_template("views/set-up-your-profile.html", form=form)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import itertools
|
||||
import time
|
||||
import uuid
|
||||
from string import ascii_uppercase
|
||||
@@ -6,6 +5,7 @@ from zipfile import BadZipFile
|
||||
|
||||
from flask import abort, flash, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
||||
from notifications_utils.insensitive_dict import InsensitiveDict
|
||||
@@ -52,12 +52,14 @@ def get_example_csv_fields(column_headers, use_example_as_example, submitted_fie
|
||||
|
||||
def get_example_csv_rows(template, use_example_as_example=True, submitted_fields=False):
|
||||
return {
|
||||
"email": ["test@example.com"]
|
||||
if use_example_as_example
|
||||
else [current_user.email_address],
|
||||
"sms": ["12223334444"]
|
||||
if use_example_as_example
|
||||
else [current_user.mobile_number],
|
||||
"email": (
|
||||
["test@example.com"]
|
||||
if use_example_as_example
|
||||
else [current_user.email_address]
|
||||
),
|
||||
"sms": (
|
||||
["12223334444"] if use_example_as_example else [current_user.mobile_number]
|
||||
),
|
||||
}[template.template_type] + get_example_csv_fields(
|
||||
(
|
||||
placeholder
|
||||
@@ -149,8 +151,11 @@ def send_messages(service_id, template_id):
|
||||
# just show the first error, as we don't expect the form to have more
|
||||
# than one, since it only has one field
|
||||
first_field_errors = list(form.errors.values())[0]
|
||||
flash(first_field_errors[0])
|
||||
|
||||
error_message = '<span class="usa-error-message">'
|
||||
error_message = f"{error_message}{first_field_errors[0]}"
|
||||
error_message = f"{error_message}</span>"
|
||||
error_message = Markup(error_message)
|
||||
flash(error_message)
|
||||
column_headings = get_spreadsheet_column_headings_from_template(template)
|
||||
|
||||
return render_template(
|
||||
@@ -465,7 +470,7 @@ def send_one_off_step(service_id, template_id, step_index):
|
||||
)
|
||||
|
||||
|
||||
def _check_messages(service_id, template_id, upload_id, preview_row):
|
||||
def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
|
||||
try:
|
||||
# The happy path is that the job doesn’t already exist, so the
|
||||
# API will return a 404 and the client will raise HTTPError.
|
||||
@@ -502,21 +507,34 @@ def _check_messages(service_id, template_id, upload_id, preview_row):
|
||||
template = get_template(
|
||||
db_template,
|
||||
current_service,
|
||||
show_recipient=True,
|
||||
show_recipient=False,
|
||||
email_reply_to=email_reply_to,
|
||||
sms_sender=sms_sender,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
allow_list = []
|
||||
if current_service.trial_mode:
|
||||
# Adding the simulated numbers to allow list
|
||||
# so they can be sent in trial mode
|
||||
for user in Users(service_id):
|
||||
allow_list.extend([user.name, user.mobile_number, user.email_address])
|
||||
# Failed sms number
|
||||
allow_list.extend(
|
||||
["simulated user (fail)", "+14254147167", "simulated@simulated.gov"]
|
||||
)
|
||||
# Success sms number
|
||||
allow_list.extend(
|
||||
["simulated user (success)", "+14254147755", "simulatedtwo@simulated.gov"]
|
||||
)
|
||||
else:
|
||||
allow_list = None
|
||||
recipients = RecipientCSV(
|
||||
contents,
|
||||
template=template,
|
||||
max_initial_rows_shown=50,
|
||||
max_errors_shown=50,
|
||||
guestlist=itertools.chain.from_iterable(
|
||||
[user.name, user.mobile_number, user.email_address]
|
||||
for user in Users(service_id)
|
||||
)
|
||||
if current_service.trial_mode
|
||||
else None,
|
||||
guestlist=allow_list,
|
||||
remaining_messages=remaining_messages,
|
||||
allow_international_sms=current_service.has_permission("international_sms"),
|
||||
)
|
||||
@@ -526,11 +544,20 @@ def _check_messages(service_id, template_id, upload_id, preview_row):
|
||||
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
|
||||
)
|
||||
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,
|
||||
)
|
||||
choose_time_form = ChooseTimeForm()
|
||||
|
||||
if preview_row < 2:
|
||||
@@ -558,11 +585,13 @@ def _check_messages(service_id, template_id, upload_id, preview_row):
|
||||
remaining_messages=remaining_messages,
|
||||
choose_time_form=choose_time_form,
|
||||
back_link=back_link,
|
||||
back_link_from_preview=back_link_from_preview,
|
||||
first_recipient_column=recipients.recipient_column_headers[0],
|
||||
preview_row=preview_row,
|
||||
sent_previously=job_api_client.has_sent_previously(
|
||||
service_id, template.id, db_template["version"], original_file_name
|
||||
),
|
||||
template_id=template_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -610,13 +639,34 @@ def check_messages(service_id, template_id, upload_id, row_index=2):
|
||||
return render_template("views/check/ok.html", **data)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/preview",
|
||||
methods=["POST"],
|
||||
)
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/preview/row-<int:row_index>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def preview_job(service_id, template_id, upload_id, row_index=2):
|
||||
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
||||
data = _check_messages(service_id, template_id, upload_id, row_index, force_hide_sender=True)
|
||||
|
||||
return render_template(
|
||||
"views/check/preview.html",
|
||||
scheduled_for=session["scheduled_for"],
|
||||
**data,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def start_job(service_id, upload_id):
|
||||
scheduled_for = session.pop("scheduled_for", None)
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for=request.form.get("scheduled_for", ""),
|
||||
scheduled_for=scheduled_for,
|
||||
)
|
||||
|
||||
session.pop("sender_id", None)
|
||||
@@ -675,7 +725,20 @@ def get_send_test_page_title(template_type, entering_recipient, name=None):
|
||||
return "Personalize this message"
|
||||
|
||||
|
||||
def get_back_link(service_id, template, step_index, placeholders=None):
|
||||
def get_back_link(
|
||||
service_id,
|
||||
template,
|
||||
step_index,
|
||||
placeholders=None,
|
||||
preview=False,
|
||||
):
|
||||
if preview:
|
||||
return url_for(
|
||||
"main.check_notification",
|
||||
service_id=service_id,
|
||||
template_id=template.id,
|
||||
)
|
||||
|
||||
if step_index == 0:
|
||||
if should_skip_template_page(template._template):
|
||||
return url_for(
|
||||
@@ -754,11 +817,11 @@ def send_one_off_to_myself(service_id, template_id):
|
||||
def check_notification(service_id, template_id):
|
||||
return render_template(
|
||||
"views/notifications/check.html",
|
||||
**_check_notification(service_id, template_id),
|
||||
**_check_notification(service_id, template_id, show_recipient=True),
|
||||
)
|
||||
|
||||
|
||||
def _check_notification(service_id, template_id, exception=None):
|
||||
def _check_notification(service_id, template_id, exception=None, **kwargs):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
@@ -771,15 +834,20 @@ def _check_notification(service_id, template_id, exception=None):
|
||||
template = get_template(
|
||||
db_template,
|
||||
current_service,
|
||||
show_recipient=True,
|
||||
email_reply_to=email_reply_to,
|
||||
sms_sender=sms_sender,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
placeholders = fields_to_fill_in(template)
|
||||
|
||||
back_link = get_back_link(service_id, template, len(placeholders), placeholders)
|
||||
|
||||
back_link_from_preview = get_back_link(
|
||||
service_id, template, len(placeholders), placeholders, preview=True
|
||||
)
|
||||
|
||||
choose_time_form = ChooseTimeForm()
|
||||
|
||||
if (not session.get("recipient")) or not all_placeholders_in_session(
|
||||
template.placeholders
|
||||
):
|
||||
@@ -791,6 +859,8 @@ def _check_notification(service_id, template_id, exception=None):
|
||||
return dict(
|
||||
template=template,
|
||||
back_link=back_link,
|
||||
back_link_from_preview=back_link_from_preview,
|
||||
choose_time_form=choose_time_form,
|
||||
**(get_template_error_dict(exception) if exception else {}),
|
||||
)
|
||||
|
||||
@@ -821,12 +891,39 @@ def get_template_error_dict(exception):
|
||||
}
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check/preview",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def preview_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
if not recipient:
|
||||
return redirect(
|
||||
url_for(
|
||||
".send_one_off",
|
||||
service_id=service_id,
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
|
||||
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
||||
|
||||
return render_template(
|
||||
"views/notifications/preview.html",
|
||||
**_check_notification(service_id, template_id, show_recipient=False, force_hide_sender=True),
|
||||
scheduled_for=session["scheduled_for"],
|
||||
recipient=recipient,
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_notification(service_id, template_id):
|
||||
scheduled_for = session.pop("scheduled_for", "")
|
||||
recipient = get_recipient()
|
||||
if not recipient:
|
||||
return redirect(
|
||||
@@ -861,7 +958,7 @@ def send_notification(service_id, template_id):
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for="",
|
||||
scheduled_for=scheduled_for,
|
||||
template_id=template_id,
|
||||
original_file_name=filename,
|
||||
notification_count=1,
|
||||
@@ -877,14 +974,20 @@ def send_notification(service_id, template_id):
|
||||
service_id, job_id=upload_id, include_one_off=True
|
||||
)
|
||||
attempts = 0
|
||||
while notifications["total"] == 0 and attempts < 5:
|
||||
|
||||
# The response can come back in different forms of incompleteness
|
||||
while (
|
||||
notifications["total"] == 0
|
||||
and notifications["notifications"] == []
|
||||
and attempts < 50
|
||||
):
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
service_id, job_id=upload_id, include_one_off=True
|
||||
)
|
||||
time.sleep(0.1)
|
||||
attempts = attempts + 1
|
||||
|
||||
if notifications["total"] == 0 and attempts == 5:
|
||||
if notifications["total"] == 0 and attempts == 50:
|
||||
# This shows the job we auto-generated for the user
|
||||
return redirect(
|
||||
url_for(
|
||||
@@ -896,8 +999,9 @@ def send_notification(service_id, template_id):
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".view_notification",
|
||||
".view_job",
|
||||
service_id=service_id,
|
||||
job_id=upload_id,
|
||||
from_job=upload_id,
|
||||
notification_id=notifications["notifications"][0]["id"],
|
||||
# used to show the final step of the tour (help=3) or not show
|
||||
|
||||
@@ -477,9 +477,11 @@ def service_edit_email_reply_to(service_id, reply_to_email_id):
|
||||
current_service.id,
|
||||
reply_to_email_id=reply_to_email_id,
|
||||
email_address=form.email_address.data,
|
||||
is_default=True
|
||||
if reply_to_email_address["is_default"]
|
||||
else form.is_default.data,
|
||||
is_default=(
|
||||
True
|
||||
if reply_to_email_address["is_default"]
|
||||
else form.is_default.data
|
||||
),
|
||||
)
|
||||
return redirect(url_for(".service_email_reply_to", service_id=service_id))
|
||||
try:
|
||||
@@ -499,9 +501,11 @@ def service_edit_email_reply_to(service_id, reply_to_email_id):
|
||||
".service_verify_reply_to_address",
|
||||
service_id=service_id,
|
||||
notification_id=notification_id,
|
||||
is_default=True
|
||||
if reply_to_email_address["is_default"]
|
||||
else form.is_default.data,
|
||||
is_default=(
|
||||
True
|
||||
if reply_to_email_address["is_default"]
|
||||
else form.is_default.data
|
||||
),
|
||||
replace=reply_to_email_id,
|
||||
)
|
||||
)
|
||||
@@ -702,9 +706,11 @@ def service_edit_sms_sender(service_id, sms_sender_id):
|
||||
service_api_client.update_sms_sender(
|
||||
current_service.id,
|
||||
sms_sender_id=sms_sender_id,
|
||||
sms_sender=sms_sender["sms_sender"]
|
||||
if is_inbound_number
|
||||
else form.sms_sender.data.replace("\r", ""),
|
||||
sms_sender=(
|
||||
sms_sender["sms_sender"]
|
||||
if is_inbound_number
|
||||
else form.sms_sender.data.replace("\r", "")
|
||||
),
|
||||
is_default=True if sms_sender["is_default"] else form.is_default.data,
|
||||
)
|
||||
return redirect(url_for(".service_sms_senders", service_id=service_id))
|
||||
|
||||
@@ -16,10 +16,12 @@ from flask import (
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
from app import login_manager, user_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import LoginForm
|
||||
from app.main.views.index import error
|
||||
from app.main.views.verify import activate_user
|
||||
from app.models.user import InvitedUser, User
|
||||
from app.utils import hide_from_search_engines
|
||||
@@ -53,22 +55,19 @@ def _get_access_token(code, state):
|
||||
# JWT expiration time (10 minute maximum)
|
||||
"exp": int(time.time()) + (10 * 60),
|
||||
}
|
||||
current_app.logger.warning(f"Here is the raw payload {payload}")
|
||||
token = jwt.encode(payload, keystring, algorithm="RS256")
|
||||
base_url = f"{access_token_url}?"
|
||||
cli_assert = f"client_assertion={token}"
|
||||
cli_assert_type = "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer"
|
||||
code_param = f"code={code}"
|
||||
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
||||
current_app.logger.info(f"This is the url we use to get the access token: {url}")
|
||||
headers = {"Authorization": "Bearer %s" % token}
|
||||
response = requests.post(url, headers=headers)
|
||||
current_app.logger.info(f"GOT A RESPONSE {response.json()}")
|
||||
access_token = response.json()["access_token"]
|
||||
return access_token
|
||||
|
||||
|
||||
def _get_user_email(access_token):
|
||||
def _get_user_email_and_uuid(access_token):
|
||||
headers = {"Authorization": "Bearer %s" % access_token}
|
||||
user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL")
|
||||
user_attributes = requests.get(
|
||||
@@ -76,24 +75,28 @@ def _get_user_email(access_token):
|
||||
headers=headers,
|
||||
)
|
||||
user_email = user_attributes.json()["email"]
|
||||
return user_email
|
||||
user_uuid = user_attributes.json()["sub"]
|
||||
return user_email, user_uuid
|
||||
|
||||
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in():
|
||||
def _do_login_dot_gov():
|
||||
# start login.gov
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
login_gov_error = request.args.get("error")
|
||||
if code and state:
|
||||
access_token = _get_access_token(code, state)
|
||||
user_email = _get_user_email(access_token)
|
||||
user_email, user_uuid = _get_user_email_and_uuid(access_token)
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
# activate the user
|
||||
user = user_api_client.get_user_by_email(user_email)
|
||||
activate_user(user["id"])
|
||||
try:
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
activate_user(user["id"])
|
||||
except BaseException as be: # noqa B036
|
||||
current_app.logger.error(be)
|
||||
error(401)
|
||||
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
|
||||
elif login_gov_error:
|
||||
@@ -101,6 +104,11 @@ def sign_in():
|
||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||
# end login.gov
|
||||
|
||||
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in():
|
||||
_do_login_dot_gov()
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
@@ -167,40 +175,26 @@ def sign_in():
|
||||
)
|
||||
|
||||
other_device = current_user.logged_in_elsewhere()
|
||||
notify_env = os.getenv("NOTIFY_ENVIRONMENT")
|
||||
current_app.logger.info("should render the sign in template")
|
||||
|
||||
# TODO REMOVE THIS INFO ONCE STAGING WORKS WITH LOGIN DOT GOV
|
||||
current_app.logger.info(f"NOTIFY ENV = {notify_env}")
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_CLIENT_ID={os.getenv('LOGIN_DOT_GOV_CLIENT_ID')}"
|
||||
token = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_USER_INFO_URL={os.getenv('LOGIN_DOT_GOV_USER_INFO_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_ACCESS_TOKEN_URL={os.getenv('LOGIN_DOT_GOV_ACCESS_TOKEN_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_LOGOUT_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_BASE_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_BASE_LOGOUT_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_SIGNOUT_REDIRECT={os.getenv('LOGIN_DOT_GOV_SIGNOUT_REDIRECT')}"
|
||||
)
|
||||
initial_signin_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
current_app.logger.info(f"LOGIN_DOT_GOV_INITIAL_SIGNIN_URL={initial_signin_url}")
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
# handle unit tests
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", token)
|
||||
url = url.replace("STATE", token)
|
||||
|
||||
return render_template(
|
||||
"views/signin.html",
|
||||
form=form,
|
||||
again=bool(redirect_url),
|
||||
other_device=other_device,
|
||||
login_gov_enabled=bool(notify_env in ["development", "staging"]),
|
||||
login_gov_enabled=True,
|
||||
password_reset_url=password_reset_url,
|
||||
initial_signin_url=initial_signin_url,
|
||||
initial_signin_url=url,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ from flask_login import current_user
|
||||
|
||||
from app.main import main
|
||||
|
||||
# ask login.gov if we really need manual logout and what's up with one hour sessions
|
||||
# ask login.gov how they recommend approaching dev environment
|
||||
# ask Tim Donaworth the same for #2
|
||||
|
||||
|
||||
def _sign_out_at_login_dot_gov():
|
||||
base_url = os.getenv("LOGIN_DOT_GOV_BASE_LOGOUT_URL")
|
||||
|
||||
@@ -18,10 +18,6 @@ def features_nav():
|
||||
"name": "Security",
|
||||
"link": "main.security",
|
||||
},
|
||||
{
|
||||
"name": "Terms of use",
|
||||
"link": "main.terms",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from itsdangerous import SignatureExpired
|
||||
from notifications_utils.url_safe_token import check_token
|
||||
|
||||
from app import user_api_client
|
||||
from app.extensions import redis_client
|
||||
from app.main import main
|
||||
from app.main.forms import TwoFactorForm
|
||||
from app.models.user import InvitedOrgUser, InvitedUser, User
|
||||
@@ -64,20 +65,46 @@ def verify_email(token):
|
||||
|
||||
def activate_user(user_id):
|
||||
user = User.from_id(user_id)
|
||||
# the user will have a new current_session_id set by the API - store it in the cookie for future requests
|
||||
|
||||
# This is the login.gov path
|
||||
login_gov_invite_data = redis_client.get(f"service-invite-{user.email_address}")
|
||||
if login_gov_invite_data:
|
||||
login_gov_invite_data = json.loads(login_gov_invite_data.decode("utf8"))
|
||||
|
||||
# This is the deprecated path for organization invites where we get id from session
|
||||
session["current_session_id"] = user.current_session_id
|
||||
organization_id = session.get("organization_id")
|
||||
|
||||
activated_user = user.activate()
|
||||
activated_user.login()
|
||||
|
||||
# TODO when login.gov is mandatory, get rid of the if clause, it is deprecated.
|
||||
invited_user = InvitedUser.from_session()
|
||||
if invited_user:
|
||||
service_id = _add_invited_user_to_service(invited_user)
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
elif login_gov_invite_data:
|
||||
service_id = login_gov_invite_data["service_id"]
|
||||
|
||||
user.add_to_service(
|
||||
service_id,
|
||||
login_gov_invite_data["permissions"],
|
||||
login_gov_invite_data["folder_permissions"],
|
||||
login_gov_invite_data["from_user_id"],
|
||||
)
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
|
||||
# TODO when login.gov is mandatory, git rid of the if clause, it is deprecated.
|
||||
invited_org_user = InvitedOrgUser.from_session()
|
||||
if invited_org_user:
|
||||
user_api_client.add_user_to_organization(invited_org_user.organization, user_id)
|
||||
elif redis_client.get(f"organization-invite-{user.email_address}"):
|
||||
organization_id = redis_client.raw_get(
|
||||
f"organization-invite-{user.email_address}"
|
||||
)
|
||||
user_api_client.add_user_to_organization(
|
||||
organization_id.decode("utf8"), user_id
|
||||
)
|
||||
|
||||
if organization_id:
|
||||
return redirect(url_for("main.organization_dashboard", org_id=organization_id))
|
||||
|
||||
@@ -45,7 +45,6 @@ class HeaderNavigation(Navigation):
|
||||
"features_sms",
|
||||
"roadmap",
|
||||
"security",
|
||||
"terms",
|
||||
},
|
||||
"using_notify": {
|
||||
"get_started",
|
||||
|
||||
@@ -44,6 +44,16 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
user_data = self.post("/user/email", data={"email": email_address})
|
||||
return user_data["data"]
|
||||
|
||||
def get_user_by_uuid_or_email(self, user_uuid, email_address):
|
||||
|
||||
user_data = self.post(
|
||||
"/user/get-login-gov-user",
|
||||
data={"login_uuid": user_uuid, "email": email_address},
|
||||
)
|
||||
if user_data is None or user_data.get("data") is None:
|
||||
return None
|
||||
return user_data["data"]
|
||||
|
||||
def get_user_by_email_or_none(self, email_address):
|
||||
try:
|
||||
return self.get_user_by_email(email_address)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{% endblock %}
|
||||
{% block meta %}
|
||||
<meta property="og:site_name" content="Notify.gov">
|
||||
<meta property="og:image" content="{{ asset_url('images/usa-opengraph-image.png') }}">
|
||||
<meta property="og:image" content="{{ asset_url('images/notify-og-image.png') }}">
|
||||
{% endblock %}
|
||||
<script type="text/javascript" src="{{ asset_url('js/gtm_head.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -175,11 +175,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
|
||||
<!-- lines 179 - 251 can all be deleted and replaced with {% include "new/components/footer.html" %} -->
|
||||
{% if current_service and current_service.research_mode %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a><span id="research-mode" class="research-mode">research mode</span>' %}
|
||||
{% else %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set commit_hash = ", Latest version: " + config['COMMIT_HASH'] %}
|
||||
{% set long_link = '<a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||
{% endif %}
|
||||
|
||||
{{ usaFooter({
|
||||
@@ -201,10 +203,6 @@
|
||||
"href": url_for("main.security"),
|
||||
"text": "Security"
|
||||
},
|
||||
{
|
||||
"href": url_for("main.terms"),
|
||||
"text": "Terms of use"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -244,7 +242,7 @@
|
||||
{
|
||||
"href": url_for('main.support'),
|
||||
"text": "Contact us"
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
@@ -253,7 +251,6 @@
|
||||
"html": meta_suffix
|
||||
}
|
||||
}) }}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% block sessionUserWarning %}
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timerWarning">
|
||||
|
||||
@@ -4,38 +4,6 @@
|
||||
</div> #}
|
||||
<div class="usa-footer__primary-section">
|
||||
<div class="grid-container">
|
||||
<div class="grid-row">
|
||||
<div class="grid-col">
|
||||
{# {% if params.navigation %}
|
||||
<nav class="usa-footer__nav" aria-label="Footer navigation,,">
|
||||
<div class="grid-row grid-gap-2">
|
||||
{% for nav in params.navigation %}
|
||||
<div class="mobile-lg:grid-col-6 desktop:grid-col-4">
|
||||
<section class="
|
||||
usa-footer__primary-content
|
||||
usa-footer__primary-content--collapsible
|
||||
">
|
||||
<h4 class="usa-footer__primary-link">{{ nav.title }}</h4>
|
||||
<ul class="usa-list usa-list--unstyled">
|
||||
{% for item in nav.items %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-footer__secondary-link">
|
||||
<a href="{{ item.href }}" {% for attribute, value in item.attributes %}
|
||||
{{attribute}}="{{value}}" {% endfor %}>
|
||||
{{ item.text }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %} #}
|
||||
</div>
|
||||
</div>
|
||||
<div class="padding-y-1">
|
||||
{% if params.meta %}
|
||||
<h2 class="usa-sr-only">{{ params.meta.visuallyHiddenTitle | default("Support links") }}</h2>
|
||||
@@ -114,4 +82,4 @@
|
||||
<a href="https://www.usa.gov/" class="usa-link">Visit USA.gov</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,61 +44,62 @@
|
||||
|
||||
<!-- usa header -->
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center" id="-logo">
|
||||
{# <div class="logo-img display-flex">
|
||||
<span class="usa-sr-only">US Notify Logo</span>
|
||||
<image src="{{ params.assetsPath | default('/static/images') }}/us-notify-color.png" alt="US Notify logo" xlink:href=""
|
||||
class="usa-flag-logo margin-right-1" width="40" height="35"></image>
|
||||
</div> #}
|
||||
<em class="logo-text usa-logo__text">
|
||||
<a href="/" title="notify.gov">notify.gov</a>
|
||||
</em>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in params.navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-2">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% for item in params.secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- <section aria-label="Search component">
|
||||
<form class="usa-search usa-search--small margin-bottom-2" role="search">
|
||||
<label class="usa-sr-only" for="search-field">Search</label>
|
||||
<input class="usa-input" id="search-field" type="search" name="search" />
|
||||
<button class="usa-button" type="submit">
|
||||
<img src="/static/images/usa-icons-bg/search--white.svg" class="usa-search__submit-icon" alt="Search" />
|
||||
</button>
|
||||
</form>
|
||||
</section> -->
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<image src="{{ params.assetsPath | default('/static/images') }}/notify-logo.png" alt="Notify.gov logo" xlink:href=""
|
||||
class="usa-flag-logo margin-right-1"></image>
|
||||
</a>
|
||||
</div>
|
||||
{% if params.navigation %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in params.navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% for item in params.secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- <section aria-label="Search component">
|
||||
<form class="usa-search usa-search--small margin-bottom-2" role="search">
|
||||
<label class="usa-sr-only" for="search-field">Search</label>
|
||||
<input class="usa-input" id="search-field" type="search" name="search" />
|
||||
<button class="usa-button" type="submit">
|
||||
<img src="/static/images/usa-icons-bg/search--white.svg" class="usa-search__submit-icon" alt="Search" />
|
||||
</button>
|
||||
</form>
|
||||
</section> -->
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
13
app/templates/components/components/select/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Select
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the select component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/select/).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "ID for each select box."
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Name property for the select."
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
"type": "array",
|
||||
"required": true,
|
||||
"description": "The items within the select component."
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Value for the option which should be selected. Use this as an alternative to setting the selected option on each individual item."
|
||||
},
|
||||
{
|
||||
"name": "disabled",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "If true, select box will be disabled. Use the disabled option on each individual item to only disable certain options."
|
||||
},
|
||||
{
|
||||
"name": "describedBy",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users."
|
||||
},
|
||||
{
|
||||
"name": "label",
|
||||
"type": "object",
|
||||
"required": true,
|
||||
"description": "The label used by the select component.",
|
||||
"isComponent": true
|
||||
},
|
||||
{
|
||||
"name": "hint",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Can be used to add a hint to the select component.",
|
||||
"isComponent": true
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the select."
|
||||
}
|
||||
]
|
||||
3
app/templates/components/components/select/macro.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro usaSelect(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
15
app/templates/components/components/select/template.njk
Normal file
@@ -0,0 +1,15 @@
|
||||
{% set describedBy = params.describedBy if params.describedBy else "" %}
|
||||
<div class="usa-form-group">
|
||||
<label class="usa-label" for="{{ params.name }}">{{params.label}}</label>
|
||||
{% if params.hint %}
|
||||
<div class="usa-hint" id="{{ params.describedBy }}">{{ params.hint }}</div>
|
||||
{% endif %}
|
||||
<select class="usa-select {%- if params.classes %} {{ params.classes }}{% endif %}" id="{{ params.id }}" name="{{ params.name }}"
|
||||
{%- if params.value %} value="{{ params.value}}"{% endif %}
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
|
||||
{% for item in params.items %}
|
||||
<option value="{{ item.value }}" {%- if item.disabled == true %} selected disabled {% endif %}>{{ item.text }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -171,8 +171,8 @@
|
||||
{% endif %}
|
||||
<p class="status-hint margin-0 width-card ">
|
||||
{{ notification.status|format_notification_status_as_time(
|
||||
notification.created_at|format_datetime_short,
|
||||
(notification.updated_at or notification.created_at)|format_datetime_short
|
||||
notification.created_at|format_datetime_table,
|
||||
(notification.sent_at or notification.created_at)|format_datetime_table
|
||||
) }}
|
||||
</p>
|
||||
{% if displayed_on_single_line %}</span>{% endif %}
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
<div class="grid-row">
|
||||
|
||||
{% if navigation_links %}
|
||||
<div class="tablet:grid-col-2">
|
||||
<div class="tablet:grid-col-2 margin-bottom-4">
|
||||
{{ sub_navigation(navigation_links) }}
|
||||
</div>
|
||||
<div class="tablet:grid-col-10 padding-left-4 usa-prose site-prose">
|
||||
<div class="tablet:grid-col-10 tablet:padding-left-4 usa-prose site-prose">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-10">
|
||||
{% endif %}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-8">
|
||||
<h1>You’re not authorized to see this page</h1>
|
||||
<p class="usa-body"><a class="usa-link" href="{{ url_for('main.sign_in' )}}">Sign in</a> to Notify.gov and try again.</p>
|
||||
<p class="usa-body">If you have been invited to join Notify.gov, <a class="usa-link" href="{{ url_for('main.sign_in' )}}">sign in</a> to Notify.gov using your Login.gov account and try again.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
e
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% if help %}
|
||||
{% include 'partials/tour.html' %}
|
||||
{% else %}
|
||||
<nav class="nav">
|
||||
<nav class="nav margin-bottom-4">
|
||||
<a class="usa-button margin-top-1 margin-bottom-5 width-full"
|
||||
href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a>
|
||||
<ul class="usa-sidenav">
|
||||
@@ -14,7 +14,7 @@
|
||||
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
|
||||
{% endif %}
|
||||
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||
|
||||
@@ -19,17 +19,22 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
{% block headIcons %}
|
||||
<link rel="shortcut icon" sizes="16x16 32x32 48x48" href="{{ assetPath | default('/assets') }}/images/favicon.ico" type="image/x-icon" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/usa-mask-icon.svg" color="{{ themeColor | default('#F0F0F0') }}">
|
||||
<link rel="shortcut icon" href="{{ assetPath | default('/assets') }}/images/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32"href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png"/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<link href="{{ assetPath | default('/assets') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
{# The default og:image is added below head so that scrapers see any custom metatags first, and this is just a fallback #}
|
||||
{% block meta %}
|
||||
<meta property="og:site_name" content="Notify.gov">
|
||||
<meta property="og:image" content="{{ asset_url('images/usa-opengraph-image.png') }}">
|
||||
<meta property="og:image" content="{{ asset_url('images/notify-og-image.png') }}">
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
|
||||
126
app/templates/new/base.html
Normal file
@@ -0,0 +1,126 @@
|
||||
{% from "../components/banner.html" import banner %}
|
||||
{% from "../components/components/skip-link/macro.njk" import usaSkipLink -%}
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ htmlLang | default('en') }}" class="{{ htmlClasses }}">
|
||||
{% include "new/components/head.html" %}
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||
{% block bodyStart %}
|
||||
{% block extra_javascripts_before_body %}
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block skipLink %}
|
||||
{{ usaSkipLink({
|
||||
"href": '#main-content',
|
||||
"text": 'Skip to main content'
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% include 'new/components/usa_banner.html' %}
|
||||
{% include 'new/components/header.html' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
{% block mainClasses %}
|
||||
<!-- notes set mainClasses = "margin-top-5 padding-bottom-5" where withoutnav_template was used and maybe templates that are using content_template -->
|
||||
<main class="{{ mainClasses }}" id="main-content" role="main">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% block flash_messages %}
|
||||
<!-- flash_message.html was from the withoutnav_template and is only included on child templates that was using withoutnav_template. Now, we can add in flash_message blocks and include 'flash_messages.html' to child templates that was using withoutnav_template. This will help to eliminate the use of a whole other parent template. -->
|
||||
{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
{% block fromContentTemplatetwoColumnGrid %}
|
||||
<div class="grid-row">
|
||||
{% if navigation_links %}
|
||||
<div class="tablet:grid-col-2">
|
||||
{{ sub_navigation(navigation_links) }}
|
||||
</div>
|
||||
<div class="tablet:grid-col-10 padding-left-4 usa-prose site-prose">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-10">
|
||||
{% endif %}
|
||||
{% block content_column_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- content_column_content block is from the content_template.html. We do not need this template. We can consolidate and move it to the base.html template. We can call on this block where child templates were using the content_template.html -->
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
|
||||
{% if current_service and current_service.research_mode %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a><span id="research-mode" class="research-mode">research mode</span>' %}
|
||||
{% else %}
|
||||
{% set commit_hash = ", Latest version: " + config['COMMIT_HASH'] %}
|
||||
{% set long_link = '<a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||
{% endif %}
|
||||
|
||||
{% include "new/components/footer.html" %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% block sessionUserWarning %}
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timerWarning">
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="sessionTimerHeading">
|
||||
Your session will end soon.
|
||||
<span class="usa-sr-only">Please choose to extend your session or sign out. Your session will expire in 5 minutes or less.</span>
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p>You have been inactive for too long.
|
||||
Your session will expire in <span id="timeLeft" role="timer"></span>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button type="button" class="usa-button" id="extendSessionTimer" data-close-modal>
|
||||
Extend Session
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button type="button" class="usa-button usa-button--unstyled padding-105 text-center" id="logOutTimer"
|
||||
data-close-modal>
|
||||
Sign out
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyEnd %}
|
||||
{% block extra_javascripts %}
|
||||
{% endblock %}
|
||||
<!--[if gt IE 8]><!-->
|
||||
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
<!--<![endif]-->
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
71
app/templates/new/components/footer.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<footer class="usa-footer usa-footer--big">
|
||||
<div class="usa-footer__primary-section">
|
||||
<div class="grid-container">
|
||||
<div class="padding-y-1">
|
||||
{% if meta_suffix %}
|
||||
<h2 class="usa-sr-only">Support links</h2>
|
||||
{% if meta_suffix %}
|
||||
<div>
|
||||
{{ meta_suffix | safe if meta_suffix else '' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usa-identifier site-identifier">
|
||||
<section class="usa-identifier__section usa-identifier__section--masthead" aria-label="Agency identifier,,,,,,">
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__logos"><a href="javascript:void(0);" class="usa-identifier__logo">
|
||||
<img class="usa-identifier__logo-img" src="{{ (asset_path | default('/static')) + 'images/gsa-logo.svg' }}" alt="GSA Logo" role="img" width="48" height="48">
|
||||
</a></div>
|
||||
<section class="usa-identifier__identity" aria-label="Agency description">
|
||||
<p class="usa-identifier__identity-domain">beta.notify.gov</p>
|
||||
<p class="usa-identifier__identity-disclaimer">An official website of the <a href="https://gsa.gov">General Services Administration</a></p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="usa-identifier__section usa-identifier__section--required-links" aria-label="Important links">
|
||||
<div class="usa-identifier__container">
|
||||
<ul class="usa-identifier__required-links-list">
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/about-us" class="usa-identifier__required-link">About GSA</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/website-information/accessibility-aids"
|
||||
class="usa-identifier__required-link">Accessibility support</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/freedom-of-information-act-foia"
|
||||
class="usa-identifier__required-link">FOIA requests</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/civil-rights-programs/notification-and-federal-employee-antidiscrimination-and-retaliation-act-of-2002"
|
||||
class="usa-identifier__required-link">No FEAR Act data</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsaig.gov/" class="usa-identifier__required-link">Office of the Inspector General</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/reports/budget-performance"
|
||||
class="usa-identifier__required-link">Performance reports</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/website-information/website-policies"
|
||||
class="usa-identifier__required-link">Privacy policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="usa-identifier__section usa-identifier__section--usagov"
|
||||
aria-label="Government information and services">
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__usagov-description">
|
||||
Looking for U.S. government information and services?
|
||||
</div>
|
||||
<a href="https://www.usa.gov/" class="usa-link">Visit USA.gov</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</footer>
|
||||
46
app/templates/new/components/head.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>{% block pageTitle %}Notify.gov{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Reach people where they are with a text message platform built just for government">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="f0f0f0" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="1b1b1b" />
|
||||
{% if config['NR_MONITOR_ON'] %}
|
||||
{% include "partials/newrelic.html" -%}
|
||||
{% endif %}
|
||||
|
||||
{# Ensure that older IE versions always render with the correct rendering engine #}
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
{% block headIcons %}
|
||||
<link rel="shortcut icon" href="{{ assetPath | default('/assets') }}/images/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<link href="{{ assetPath | default('/assets') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% endblock %}
|
||||
|
||||
<link rel="stylesheet" media="screen" href="{{ asset_url('css/styles.css') }}" />
|
||||
{% block extra_stylesheets %}{% endblock %}
|
||||
|
||||
{% block meta_format_detection %}
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
{% endblock %}
|
||||
{% block og_image %}
|
||||
<meta property="og:title" content="Notify.gov">
|
||||
<meta property="og:description" content="Reach people where they are with a text message platform built just for government">
|
||||
<meta property="og:image" content="/static/images/notify-og-image.png">
|
||||
{% endblock %}
|
||||
{# google #}
|
||||
<script type="text/javascript" src="{{ asset_url('js/gtm_head.js') }}"></script>
|
||||
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
|
||||
{% if g.hide_from_search_engines %}
|
||||
<meta name="robots" content="noindex" />
|
||||
{% endif %}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
76
app/templates/new/components/header.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{# setting navigation and secondarynavigation #}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": header_navigation.is_selected('accounts-or-dashboard')},
|
||||
{"href": url_for('main.get_started'), "text": "Using Notify", "active": header_navigation.is_selected('using_notify')},
|
||||
{"href": url_for('main.features'), "text": "Features", "active": header_navigation.is_selected('features')},
|
||||
{"href": url_for('main.support'), "text": "Contact us", "active": header_navigation.is_selected('support')}
|
||||
] %}
|
||||
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
{# usa header #}
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.png' }}" alt="Notify.gov logo" class="usa-flag-logo margin-right-1">
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{% if navigation %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% if secondaryNavigation %}
|
||||
{% for item in secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
44
app/templates/new/components/usa_banner.html
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
{# usa banner #}
|
||||
<section class="usa-banner site-banner" aria-label="Official website of the United States government,,,,,,">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
<div class="usa-banner__inner">
|
||||
<div class="grid-col-auto">
|
||||
<img aria-hidden="true" class="usa-banner__header-flag" src="/static/img/us_flag_small.png" alt="" />
|
||||
</div>
|
||||
<div class="grid-col-fill tablet:grid-col-auto" aria-hidden="true">
|
||||
<p class="usa-banner__header-text">An official website of the United States government</p>
|
||||
<p class="usa-banner__header-action">Here’s how you know</p>
|
||||
</div>
|
||||
<button type="button" class="usa-accordion__button usa-banner__button" aria-expanded="false" aria-controls="gov-banner">
|
||||
<span class="usa-banner__button-text">Here’s how you know</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="usa-banner__content usa-accordion__content" id="gov-banner" hidden="">
|
||||
<div class="grid-row grid-gap-lg">
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-dot-gov.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Official websites use .gov</strong>
|
||||
<br>
|
||||
A <strong>.gov</strong> website belongs to an official government organization in the United States.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-https.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Secure .gov websites use HTTPS</strong>
|
||||
<br>
|
||||
A <strong>lock</strong> ( <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" width="52" height="64" viewBox="0 0 52 64" class="usa-banner__lock-image" role="img" aria-labelledby="banner-lock-description" focusable="false"><title id="banner-lock-title">Lock</title><desc id="banner-lock-description">Locked padlock</desc><path fill="#000000" fill-rule="evenodd" d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"></path></svg></span> ) or <strong>https://</strong> means you’ve safely connected to the .gov website. Share sensitive information only on official, secure websites.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
35
app/templates/new/layouts/org_template.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{% block org_page_title %}{% endblock %} – {{ current_org.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
<div class="navigation-service usa-breadcrumb">
|
||||
{% if current_user.platform_admin %}
|
||||
<a href="{{ url_for('.organizations') }}" class="usa-link navigation-organization-link">Organizations</a>
|
||||
{% endif %}
|
||||
<div class="navigation-service">
|
||||
{{ current_org.name }}
|
||||
</div>
|
||||
<a href="{{ url_for('main.choose_account') }}" class="usa-link navigation-service">Switch service</a>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-3">
|
||||
{% include "org_nav.html" %}
|
||||
</div>
|
||||
<div class="grid-col-9">
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
<main class="main" id="main-content" role="main" >
|
||||
{% block content %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% block maincolumn_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
app/templates/new/layouts/withnav_template.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{% block service_page_title %}{% endblock %} – {{ current_service.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
{% block serviceNavigation %}
|
||||
{% include "service_navigation.html" %}
|
||||
{% endblock %}
|
||||
<!-- The withnav_template can be used to replace the settings_template. when it comes to setting_template and withnav_template, this service_navigation.html is only used in withnav_template and in settings_template, it was not used. That is one out of the two differences between settings template and withnav template. Child templates that extends settings_template, include the block serviceNavigation but leave it empty. Within the app, the only pages settings_template.html is used: manage-users.html, service-settings.html, and user-profile.html -->
|
||||
<div class="grid-row margin-top-5">
|
||||
{% if help %}
|
||||
<div class="grid-col-3">
|
||||
{% else %}
|
||||
<div class="grid-col-3">
|
||||
{% endif %}
|
||||
{% block sideNavigation %}
|
||||
{% include "main_nav.html" %}
|
||||
<!-- include settings_nav.html for child templates that used settings_template -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% if help %}
|
||||
<div class="grid-col-8">
|
||||
{% else %}
|
||||
<div class="grid-col-9 padding-left-4">
|
||||
{% endif %}
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
<main id="main-content" role="main" class="usa-prose site-prose margin-bottom-10">
|
||||
{% block content %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% block maincolumn_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
38
app/templates/new/templates_glossary.md
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
# New Templates Glossary
|
||||
|
||||
This document serves as a glossary for the templates directory structure of the project.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `/templates`
|
||||
- `base.html`: The main base template from which all other templates inherit. This template is a combination of `main_template`, `admin_template`, `withoutnav_template`, `settings_templates`, and `content_template`.
|
||||
- **/layouts**: Contains shared layouts used across the site. Simply put, it defines the overall structure or skeleton of the application (less frequently revised).
|
||||
- `withnav_template.html`: A variation of the base layout that includes a sidebar.
|
||||
- `org_template.html`: A variaton of the withnav_template
|
||||
- **/components**: Houses reusable UI components that can be included in multiple templates and can be tailored with different content or links depending on the context.(more frequently revised or customized)
|
||||
- `head.html`: Template for the site's <head>, included in `base.html`.
|
||||
- `header.html`: Template for the site's header, included in `base.html`.
|
||||
- `footer.html`: Template for the site's footer, included in `base.html`.
|
||||
- **/views** (or **/pages**): Individual page templates that use the base layouts, components, and partials to present content.
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Use **inheritance** `{% extends %}` to build on base layouts.
|
||||
- Employ **components** `{% include %}` for reusable UI elements to keep the code DRY and facilitate easier updates.
|
||||
|
||||
### Observation Notes
|
||||
- The macro-options.json files in the header and footer component act as structural guides. They aren't directly used as data passed to the usaFooter function/macro. Instead, these files outline the expected properties and provide a description of their purpose. The `usaFooter` macro component is currently only invoked in the `admin_template`, which will eventually serve as the `base.html` template. This will simplify the approach when we change the footer macros to componenets by eliminating the need to dynamically pass this data from the base.html template. This is also true for `usaHeader` macro component.
|
||||
|
||||
|
||||
|
||||
### Old Layout Templates We Don't Need
|
||||
- withoutnav_template.html Delete
|
||||
- main_template.html Delete
|
||||
- settings_templates.html `withnav_template` can be used to replace `settings_template`.
|
||||
- settings_nav.html (move to /new/navigation directory)
|
||||
- main_nav.html (move to /new/navigation directory)
|
||||
- service_navigation.html (move to /new/navigation directory)
|
||||
- org_template, could be under it's own directory called /layout/organization
|
||||
- org_nav.html (move to /new/navigation directory)
|
||||
- content_template.html Delete
|
||||
@@ -1,49 +1,7 @@
|
||||
{% from "components/pill.html" import pill %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
{% if notifications_deleted %}
|
||||
<div class="grid-row ">
|
||||
{% for label, query_param, url, count in counts %}
|
||||
{% if query_param == 'pending' %}
|
||||
<div class="grid-col-3">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ query_param }}</span>
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid-col-3">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="tabs">
|
||||
{{ pill(counts, request.args.get('status', '')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="tabs">
|
||||
{{ pill(counts, request.args.get('status', '')) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
<p class="bottom-gutter hint">
|
||||
Report is {{ "{:.0f}%".format(job.percentage_complete * 0.99) }} complete…
|
||||
</p>
|
||||
{% elif notifications %}
|
||||
{% elif notifications and time_left != "Data no longer available" %}
|
||||
<p class="bottom-gutter">
|
||||
<a href="{{ download_link }}" download class="usa-link heading-small">Download this report (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
 
|
||||
<span id="time-left">{{ time_left }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
notifications,
|
||||
caption=uploaded_file_name,
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
{% set display_message_status %}
|
||||
{% if job.template.content %}
|
||||
<h2 class="sms-message-header">Message</h2>
|
||||
<div class="sms-message-wrapper">{{ current_service.name }}: {{job.template.content}}</div>
|
||||
{% endif %}
|
||||
{% if job.original_file_name %}
|
||||
<h2 class="recipient-list">Recipient list</h2>
|
||||
<div>
|
||||
<ul class="usa-icon-list">
|
||||
<li class="usa-icon-list__item">
|
||||
<img src="{{ url_for('static', filename='img/material-icons/description.svg') }}" alt="Description Icon">
|
||||
<div class="usa-icon-list__content">
|
||||
<h3>{{ job.original_file_name }}</h3>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2>Delivery Status</h2>
|
||||
{% endset %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
<p class='bottom-gutter'>
|
||||
{% if job.scheduled_for %}
|
||||
{% if job.processing_started %}
|
||||
Sent by {{ job.created_by.name }} on {{ job.processing_started|format_datetime_short }}
|
||||
{% if job.still_processing or arrived_from_preview_page_url %}
|
||||
{% if job.scheduled_for %}
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Your text has been scheduled</h4>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{display_message_status}}
|
||||
{% else %}
|
||||
Uploaded by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }}
|
||||
<div class="usa-alert usa-alert--success">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Your text has been sent</h4>
|
||||
<p class="usa-alert__text">
|
||||
{{ job.template_name }} - {{ current_service.name }} was sent on {{ job.created_at|format_datetime_normal }} by {{ job.created_by.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{display_message_status}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Sent by {{ job.created_by.name }} on {{ job.created_at|format_datetime_short }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if job.status == 'sending limits exceeded'%}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, notification_status_field %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
|
||||
<div class="ajax-block-container" aria-labelledby='pill-selected-item'>
|
||||
<div class="ajax-block-container table-wrapper" aria-labelledby='pill-selected-item'>
|
||||
<div class="dashboard-table bottom-gutter-3-2">
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
<div class="grid-container">
|
||||
<div class="grid-row margin-top-5">
|
||||
{% if help %}
|
||||
<div class="grid-col-3">
|
||||
<div class="tablet:grid-col-3">
|
||||
{% else %}
|
||||
<div class="grid-col-3">
|
||||
<div class="tablet:grid-col-3 margin-bottom-4">
|
||||
{% endif %}
|
||||
{% include "settings_nav.html" %}
|
||||
</div>
|
||||
{% if help %}
|
||||
<div class="grid-col-8">
|
||||
<div class="tablet:grid-col-8">
|
||||
{% else %}
|
||||
<div class="grid-col-9 padding-left-4">
|
||||
<div class="tablet:grid-col-9 tablet:padding-left-4">
|
||||
{% endif %}
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<div class="ajax-block-container" id='pill-selected-item'>
|
||||
|
||||
{% if notifications %}
|
||||
<div class="table-wrapper">
|
||||
<div class='dashboard-table'>
|
||||
{% endif %}
|
||||
{% call(item, row_number) list_table(
|
||||
@@ -29,6 +30,7 @@
|
||||
{% endcall %}
|
||||
{% if notifications %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_pagination %}
|
||||
|
||||
@@ -14,24 +14,26 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
{{ page_header('Callbacks') }}
|
||||
<div class="bottom-gutter-3-2 dashboard-table body-copy-table">
|
||||
{% call mapping_table(
|
||||
caption='General',
|
||||
field_headings=['Label', 'Value', 'Action'],
|
||||
field_headings_visible=False,
|
||||
caption_visible=False
|
||||
) %}
|
||||
{% call row() %}
|
||||
{{ text_field('Delivery receipts') }}
|
||||
{{ optional_text_field(delivery_status_callback, truncate=true) }}
|
||||
{{ edit_field('Change', url_for('.delivery_status_callback', service_id=current_service.id)) }}
|
||||
{% endcall %}
|
||||
<div class="table-wrapper">
|
||||
<div class="bottom-gutter-3-2 dashboard-table body-copy-table">
|
||||
{% call mapping_table(
|
||||
caption='General',
|
||||
field_headings=['Label', 'Value', 'Action'],
|
||||
field_headings_visible=False,
|
||||
caption_visible=False
|
||||
) %}
|
||||
{% call row() %}
|
||||
{{ text_field('Delivery receipts') }}
|
||||
{{ optional_text_field(delivery_status_callback, truncate=true) }}
|
||||
{{ edit_field('Change', url_for('.delivery_status_callback', service_id=current_service.id)) }}
|
||||
{% endcall %}
|
||||
|
||||
{% call row() %}
|
||||
{{ text_field('Received text messages') }}
|
||||
{{ optional_text_field(received_text_messages_callback, truncate=true) }}
|
||||
{{ edit_field('Change', url_for('.received_text_messages_callback', service_id=current_service.id)) }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% call row() %}
|
||||
{{ text_field('Received text messages') }}
|
||||
{{ optional_text_field(received_text_messages_callback, truncate=true) }}
|
||||
{{ edit_field('Change', url_for('.received_text_messages_callback', service_id=current_service.id)) }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{% endcall %}
|
||||
{% if item.expiry_date %}
|
||||
{% call field(align='right') %}
|
||||
<span class='hint'>Revoked {{ item.expiry_date|format_datetime_short }}</span>
|
||||
<span class='hint'>Revoked {{ item.expiry_date|format_datetime_table }}</span>
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{% call field(align='right', status='error') %}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
{% from "components/components/skip-link/macro.njk" import usaSkipLink %}
|
||||
@@ -9,7 +8,7 @@
|
||||
{% set file_contents_header_id = 'file-preview' %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ "Preview of {}".format(template.name) }}
|
||||
{{ "Select delivery time" }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -19,11 +18,11 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Preview of {}'.format(template.name)) }}
|
||||
{{ page_header('Select delivery time') }}
|
||||
|
||||
{{ template|string }}
|
||||
<div class="bottom-gutter-3-2">
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id)}}" class='page-footer'>
|
||||
<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={
|
||||
@@ -37,55 +36,11 @@
|
||||
{% endif %}
|
||||
|
||||
{% set button_text %}
|
||||
Send {{ count_of_recipients|message_count(template.template_type) }}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if not request.args.from_test %}
|
||||
|
||||
<h2 class="font-body-lg" id="{{ file_contents_header_id }}">{{ original_file_name }}</h2>
|
||||
|
||||
<div class="fullscreen-content" data-module="fullscreen-table">
|
||||
{% call(item, row_number) list_table(
|
||||
recipients.displayed_rows,
|
||||
caption=original_file_name,
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'<span class="usa-sr-only">Row in file</span><span aria-hidden="true">1</span>'|safe
|
||||
] + recipients.column_headers
|
||||
) %}
|
||||
{% call index_field() %}
|
||||
<span>
|
||||
{% if (item.index + 2) == preview_row %}
|
||||
{{ item.index + 2 }}
|
||||
{% else %}
|
||||
<a class="usa-link" href="{{ url_for('.check_messages', service_id=current_service.id, template_id=template.id, upload_id=upload_id, row_index=(item.index + 2), original_file_name=original_file_name) }}">{{ item.index + 2 }}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endcall %}
|
||||
{% for column in recipients.column_headers %}
|
||||
{% if item[column].ignore %}
|
||||
{{ text_field(item[column].data or '', status='default') }}
|
||||
{% else %}
|
||||
{{ text_field(item[column].data or '') }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if item[None].data %}
|
||||
{% for column in item[None].data %}
|
||||
{{ text_field(column, status='default') }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if count_of_displayed_recipients < count_of_recipients %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the first {{ count_of_displayed_recipients }} rows
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
107
app/templates/views/check/preview.html
Normal file
@@ -0,0 +1,107 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/table.html" import list_table, field, text_field, hidden_field_heading %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
{% from "components/components/skip-link/macro.njk" import usaSkipLink %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% set file_contents_header_id = 'file-preview' %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ "Preview of {}".format(template.name) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link_from_preview }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Preview') }}
|
||||
<div>
|
||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_normal if scheduled_for else 'Now'}}</p>
|
||||
<p class="sms-message-file-name">File: {{original_file_name}}</p>
|
||||
<p class="sms-message-template">Template: {{template.name}}</p>
|
||||
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
||||
</div>
|
||||
|
||||
<h2 id="{{ file_contents_header_id }}">Message</h2>
|
||||
<div class="preview-message"> {{ template|string }}</div>
|
||||
{% if not request.args.from_test %}
|
||||
<h2>Recipients list</h2>
|
||||
<div>
|
||||
<ul class="usa-icon-list">
|
||||
<li class="usa-icon-list__item">
|
||||
<img src="{{ url_for('static', filename='img/material-icons/description.svg') }}" alt="Description Icon">
|
||||
<div class="usa-icon-list__content">
|
||||
<h3>{{ original_file_name }}</h3>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="usa-table-container--scrollable" tabindex="0">
|
||||
<table class="usa-table usa-table--borderless width-full">
|
||||
<caption class="font-body-lg table-heading">
|
||||
Note: Only the first 5 rows are displayed here.
|
||||
</caption>
|
||||
<thead class="table-field-headings-visible">
|
||||
<tr>
|
||||
{% for header in recipients.column_headers %}
|
||||
<th scope="col" class="table-field-heading-first">
|
||||
{{ header }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in recipients.displayed_rows %}
|
||||
{% if loop.index <= 5 %}
|
||||
<tr class="table-row">
|
||||
{% for column in recipients.column_headers %}
|
||||
<td class="table-field-left-aligned">
|
||||
<div>
|
||||
{% set column_data = item[column].data or '' %}
|
||||
{% if column_data is iterable and column_data is not string %}
|
||||
<ul>
|
||||
{% for data_item in column_data %}
|
||||
{% if data_item is not none %}
|
||||
<li>{{ data_item }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ column_data }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% if item[None].data %}
|
||||
{% for column in item[None].data %}
|
||||
<td class="table-field-left-aligned">
|
||||
<div class="">
|
||||
{{ column }}
|
||||
</div>
|
||||
</td>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- <div class="bottom-gutter-3-2">
|
||||
<p>This is a placeholder: This message will be delivered to <b>400 phone numbers</b> and will use a total of <b>800 message parts</b>, leaving Washington DSHS with <b>249,200 message parts remaining</b>.</p>
|
||||
</div> -->
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<h3>Does everything look good?</h3>
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,115 +1,117 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
||||
|
||||
<div class='dashboard-table ajax-block-container'>
|
||||
{% call(item, row_number) list_table(
|
||||
jobs,
|
||||
caption="Recent files uploaded",
|
||||
caption_visible=False,
|
||||
empty_message=(
|
||||
'You have not uploaded any files yet.'
|
||||
),
|
||||
field_headings=[
|
||||
'File',
|
||||
'Status'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call row_heading() %}
|
||||
<div class="file-list">
|
||||
<a class="file-list-filename-large usa-link" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.original_file_name }}</a>
|
||||
{% if item.scheduled %}
|
||||
<span class="file-list-hint-large">
|
||||
Sending {{
|
||||
item.scheduled_for|format_datetime_relative
|
||||
}}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="file-list-hint-large">
|
||||
Sent {{
|
||||
(item.scheduled_for or item.created_at)|format_datetime_relative
|
||||
}}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{% if item.scheduled %}
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notification_count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, item.notification_count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(item.notification_count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ item.notification_count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if item.notification_count %}
|
||||
<span class="big-number-label">{{ item.notification_count|message_count_label(item.template_type,suffix='waiting to send') }}</span>
|
||||
{% endif %}
|
||||
<div class="table-wrapper">
|
||||
<div class='dashboard-table ajax-block-container'>
|
||||
{% call(item, row_number) list_table(
|
||||
jobs,
|
||||
caption="Recent files uploaded",
|
||||
caption_visible=False,
|
||||
empty_message=(
|
||||
'You have not uploaded any files yet.'
|
||||
),
|
||||
field_headings=[
|
||||
'File',
|
||||
'Status'
|
||||
],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call row_heading() %}
|
||||
<div class="file-list">
|
||||
<a class="file-list-filename-large usa-link" href="{{ url_for('.view_job', service_id=current_service.id, job_id=item.id) }}">{{ item.original_file_name }}</a>
|
||||
{% if item.scheduled %}
|
||||
<span class="file-list-hint-large">
|
||||
Sending {{
|
||||
item.scheduled_for|format_datetime_relative
|
||||
}}
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-4">
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="file-list-hint-large">
|
||||
Sent {{
|
||||
(item.scheduled_for or item.created_at)|format_datetime_relative
|
||||
}}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{% if item.scheduled %}
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{{ "{:,}".format(item.notifications_sending) }}
|
||||
</span>
|
||||
<span class="big-number-label">pending</span>
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid-col-4">
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notifications_delivered is number %}
|
||||
{{ "{:,}".format(item.notifications_delivered) }}
|
||||
{% else %}
|
||||
{{ item.notifications_delivered }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">delivered</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid-col-4">
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notifications_failed is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, item.notifications_failed) }}
|
||||
{% if item.notification_count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, item.notification_count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(item.notification_count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ "{:,}".format(item.notifications_failed) }}
|
||||
{{ item.notification_count }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ item.notifications_failed }}
|
||||
</span>
|
||||
{% if item.notification_count %}
|
||||
<span class="big-number-label">{{ item.notification_count|message_count_label(item.template_type,suffix='waiting to send') }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">failed</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-4">
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{{ "{:,}".format(item.notifications_sending) }}
|
||||
</span>
|
||||
<span class="big-number-label">pending</span>
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid-col-4">
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notifications_delivered is number %}
|
||||
{{ "{:,}".format(item.notifications_delivered) }}
|
||||
{% else %}
|
||||
{{ item.notifications_delivered }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">delivered</span>
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid-col-4">
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notifications_failed is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, item.notifications_failed) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(item.notifications_failed) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ item.notifications_failed }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">failed</span>
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div></div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div></div>
|
||||
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,25 @@
|
||||
<div class="ajax-block-container">
|
||||
<div class="grid-row grid-gap">
|
||||
<div id="total-sms" class="grid-col-12">
|
||||
<span class="big-number-with-status">
|
||||
<a class="usa-link display-flex" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='sending,delivered,failed') }}">
|
||||
<div id="total-sms" class="grid-col-12 margin-top-2">
|
||||
<span class="big-number-with-status display-block margin-bottom-2">
|
||||
<p>
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if statistics['sms']['requested'] is number %}
|
||||
{{ "{:,}".format(statistics['sms']['requested']) }}
|
||||
{{ "{:,}".format(statistics['sms']['requested']) }}
|
||||
{% else %}
|
||||
{{ statistics['sms']['requested'] }}
|
||||
{{ statistics['sms']['requested'] }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ statistics['sms']['requested']|message_count_label('sms', suffix='sent') }}</span>
|
||||
<span class="big-number-label">{{ statistics['sms']['requested']|message_count_label('sms', suffix='sent') }} in the last seven days</span>
|
||||
</span>
|
||||
</p>
|
||||
<a class="usa-button usa-button--outline" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='sending,delivered,failed') }}">
|
||||
Details
|
||||
</a>
|
||||
{% if show_failures %}
|
||||
<span class="big-number-status{% if statistics['sms']['show_warning'] is sameas true %} big-number-status--failing{% endif %}">
|
||||
{% if statistics['sms']['failed'] %}
|
||||
<a class="usa-link" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='failed') }}">
|
||||
{{ "{:,}".format(statistics['sms']['failed']) }}
|
||||
failed – {{ statistics['sms']['failed_percentage'] }}%
|
||||
</a>
|
||||
{% else %}
|
||||
No failures
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{# Removing the failures area for now, as the user can click on the above link to see all the details.
|
||||
In the future state of the dashboard, the all statuses will be more apparent with data visualizations #}
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div id="total-email" class="grid-col-6">
|
||||
<span class="big-number-with-status pilot-disabled">
|
||||
<a class="usa-link display-block margin-bottom-1">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">0</span>
|
||||
<span class="big-number-label">emails sent</span>
|
||||
</span>
|
||||
</a>
|
||||
<span class="big-number-status">No failures</span>
|
||||
</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Usage</th>
|
||||
<th scope="col">Sent</th>
|
||||
<th scope="col">Remaining</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -18,92 +18,93 @@
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'upcoming') }}
|
||||
|
||||
<h2 class="font-body-xl margin-0">
|
||||
<h2 class="font-body-2xl line-height-sans-2 margin-0">
|
||||
Messages sent
|
||||
</h2>
|
||||
<p class="margin-top-0">In the last seven days</p>
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'totals') }}
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Batched Jobs</h2>
|
||||
|
||||
<div class='job-table'>
|
||||
{% call(item, row_number) list_table(
|
||||
notifications,
|
||||
caption="Batched Jobs",
|
||||
caption_visible=False,
|
||||
border_visible=True,
|
||||
empty_message='No batched job messages found  (messages are kept for {} days)'.format(service_data_retention_days)|safe,
|
||||
field_headings=['Template Name','Date/Time', 'Download (CSV) Report'],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% if item.job.original_file_name and item.job.id %}
|
||||
{% call row_heading() %}
|
||||
<a class="usa-link file-list-filename" href="/services/{{ item.service }}/jobs/{{ item.job.id }}">{{ item.job.original_file_name|replace('.csv', '') if item.job.id else '' }}</a>
|
||||
{% endcall %}
|
||||
{% call row_heading() %}
|
||||
{{ item.status|format_notification_status_as_time(
|
||||
item.created_at|format_datetime_short,
|
||||
(item.updated_at or item.created_at)|format_datetime_short)
|
||||
}}
|
||||
{% endcall %}
|
||||
{% call row_heading() %}
|
||||
{% set availability = download_availability|selectattr('job_id', 'equalto', item.job.id)|first %}
|
||||
{% if availability and availability.time_left != "Data no longer available" %}
|
||||
<a class="usa-link file-list-filename" href="{{ availability.download_link[0] }}">{{ "Download" if item.job.original_file_name else '' }}</a>
|
||||
<span>- {{ availability.time_left }}</span>
|
||||
{% elif availability %}
|
||||
<span>{{ availability.time_left }}</span>
|
||||
<h2 class="margin-top-4 margin-bottom-1">Recent Batches</h2>
|
||||
<div class="table-wrapper">
|
||||
<table class="usa-table usa-table--borderless job-table">
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" class="table-field-heading-first">
|
||||
<span>File name</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Template</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Time sent</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Sender</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span># of Recipients</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading">
|
||||
<span>Report</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if job_and_notifications %}
|
||||
{% for job in job_and_notifications[:5] %}
|
||||
{% if job.job_id and job.notifications %}
|
||||
{% set notification = job.notifications[0] %}
|
||||
<tr class="table-row" id="{{ job.job_id }}">
|
||||
<td class="table-field file-name">
|
||||
{{ notification.job.original_file_name if notification.job.original_file_name else 'Manually entered number'}}
|
||||
<br>
|
||||
<a class="usa-link file-list-filename" href="{{ job.view_job_link }}">View Batch</a>
|
||||
</td>
|
||||
<td class="table-field template">
|
||||
{{ notification.template.name }}
|
||||
</td>
|
||||
<td class="table-field time-sent">
|
||||
{{ job.created_at | format_datetime_table }}
|
||||
</td>
|
||||
<td class="table-field sender">
|
||||
{{ notification.created_by.name }}
|
||||
</td>
|
||||
<td class="table-field count-of-recipients">
|
||||
{{ job.notification_count}}
|
||||
</td>
|
||||
<td class="table-field report">
|
||||
{% if notification and job.time_left != "Data no longer available" %}
|
||||
<a class="usa-link file-list-filename" href="{{ job.download_link }}">Download</a>
|
||||
<span class="usa-hint">{{ job.time_left }}</span>
|
||||
{% elif job %}
|
||||
<span>{{ job.time_left }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr class="table-row">
|
||||
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
{% if show_pagination %}
|
||||
{{ previous_next_navigation(prev_page, next_page) }}
|
||||
{% elif next_page %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the first 50 messages
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Usage</h2>
|
||||
<h3 class="margin-bottom-0">Daily</h3>
|
||||
<p class="margin-0">Across all services</p>
|
||||
<table class="usage-table usa-table usa-table--borderless margin-top-1 margin-bottom-5">
|
||||
<caption class="usa-sr-only">
|
||||
Daily
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Usage</th>
|
||||
<th scope="col">Remaining</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ global_message_limit - daily_global_messages_remaining }}</td>
|
||||
<td>
|
||||
{{ daily_global_messages_remaining }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Message count</h2>
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
<h3 class='margin-bottom-0' id="current-year"></h3>
|
||||
{{ ajax_block(partials, updates_url, 'usage') }}
|
||||
<a
|
||||
href="{{ url_for('.usage', service_id=current_service['id']) }}"
|
||||
class="usa-link show-more"
|
||||
><span>See all usage</span></a>
|
||||
<p class="margin-top-0">During the pilot period, each service has an allowance of 250,000 message parts. Once this allowance is met, the
|
||||
application will stop delivering messages. There's no monthly charge, no setup fee, and no procurement cost.</p>
|
||||
<p class="align-with-heading-copy">
|
||||
What counts as 1 text message part?<br />
|
||||
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,115 +11,232 @@
|
||||
|
||||
<h1 class="font-body-2xl margin-bottom-3">Guidance</h1>
|
||||
|
||||
<p>This guidance is for teams using Notify.gov to send text messages.</p>
|
||||
|
||||
<p>It explains how to:</p>
|
||||
<p>Notify allows you easily to create templates for messages for your recipients. You can customize messages to encourage
|
||||
your recipient to manage their benefits and increase follow through.</p>
|
||||
<p>Below we explain how to:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li><a class="usa-link" href="#editandformat">edit and format messages</a></li>
|
||||
<li><a class="usa-link" href="#customization">add customization</a></li>
|
||||
<!-- <li><a class="usa-link" href="{{ url_for('.send_files_by_email') }}">send files by email</a></li> -->
|
||||
<li><a class="usa-link" href="#format-content">Format your content</a></li>
|
||||
<li><a class="usa-link" href="#add-links">Add links</a></li>
|
||||
<li><a class="usa-link" href="#personalize-content">Personalize your content</a></li>
|
||||
<li><a class="usa-link" href="#conditional-content">Add conditional content</a></li>
|
||||
<li><a class="usa-link" href="#indentify-program">Identify your program</a></li>
|
||||
<li><a class="usa-link" href="#prepare-data">Prepare your data</a></li>
|
||||
<li><a class="usa-link" href="#prevent-fraud">Prevent fraud</a></li>
|
||||
</ul>
|
||||
|
||||
<h2 class="font-body-lg" id="#formatting">Edit and format messages</h2>
|
||||
{# Format content #}
|
||||
<h2 class="padding-top-1" id="format-content">Format your content</h2>
|
||||
<p>Effective texts will help your message recipients take the steps needed to secure and keep the benefits and services
|
||||
they depend on. To craft an effective text:</p>
|
||||
|
||||
<p>This section explains how to:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li><a class="usa-link" href="#formatting">format your content</a></li>
|
||||
<li><a class="usa-link" href="#links">add links</a></li>
|
||||
<li><a class="usa-link" href="#personalised-content">personalize your content</a></li>
|
||||
<li><a class="usa-link" href="#optional-content">add optional content</a></li>
|
||||
<li>Choose your messages thoughtfully. Text messages are best to remind/nudge someone to take a specific action or
|
||||
communicate information at a particular time rather than pushing out broad or overly general information.</li>
|
||||
<li>Clearly state the issue and expected response. Tell your recipient what you expect from them.</li>
|
||||
<li>Say only one important thing per message.</li>
|
||||
<li>Frame the message to encourage action and explain the consequences of not completing the desired action.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="font-body-lg" id="formatting">Format your content</h3>
|
||||
|
||||
<p>You can see a list of formatting instructions on the edit template page:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
<h3>To create and format your message</h3>
|
||||
<ol class="list">
|
||||
<li>All messages start from a template</li>
|
||||
<li>Click "<a href={{ url_for('.choose_template', service_id=current_service.id) }}>Send Messages</a>". You'll see existing templates.</li>
|
||||
<li>Add a new template or choose an existing template and select Edit.</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="font-body-lg" id="links">Add links</h3>
|
||||
|
||||
<p>When composing a text message, write URLs in full and Notify will convert them into links for you.
|
||||
</p>
|
||||
|
||||
<p>You cannot convert text into a link.
|
||||
<!-- Research shows that people like to check a URL looks genuine before clicking the link in an email. This is hard to do if the URL is hidden behind clickable link text.-->
|
||||
</p>
|
||||
|
||||
<p>We do not recommend using a third-party link shortening service because:</p>
|
||||
{# Add links #}
|
||||
<h2 class="padding-top-1" id="add-links">Add links</h2>
|
||||
<p>When composing a text message, links to websites or online applications can help your recipient respond quickly.</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>your users cannot see where the link will take them</li>
|
||||
<li>your link might stop working if there’s a service outage</li>
|
||||
<li>you can no longer control where the redirect goes</li>
|
||||
<li>Write URLs in full and Notify will convert them into links for you. Note that you cannot hyperlink text in Notify
|
||||
messages.</li>
|
||||
<li>For link click tracking, you can consider adding campaign parameters to URLs.</li>
|
||||
<li>All links should point to a government domain.</li>
|
||||
<li>Link directly to where your recipient needs to take action, not to more information.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="font-body-lg" id="personalised-content">Personalize your content</h3>
|
||||
<h3>About link-shortening services</h3>
|
||||
<p>We do not recommend using a third-party link-shortening service because:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>Your recipient cannot see where the link will take them, which could make them suspect the link is spam/scam.</li>
|
||||
<li>Your link might stop working if there’s a service outage.</li>
|
||||
<li>You can no longer control where the redirect goes.</li>
|
||||
</ul>
|
||||
|
||||
<p>To personalize the content of your messages, add a placeholder to the template.</p>
|
||||
{# Personalize content #}
|
||||
<h2 class="padding-top-1" id="personalize-content">Personalize your content</h2>
|
||||
<p>Personalizing your content can increase response rates and help the recipient know the text is legitimate.</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>Including a person's first name increases response rates.</li>
|
||||
<li>Specific details such as time and location of an appointment or where suspected fraud use occurred encourages action.</li>
|
||||
</ul>
|
||||
|
||||
<p>Placeholders are filled in with details, like a name or reference number, each time you send a
|
||||
message.</p>
|
||||
|
||||
<p>To add a placeholder to the template:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
<li>Add a placeholder using double brackets. For example: Hello ((first name)), your reference is
|
||||
((ref number)).</li>
|
||||
<li>Select <b class="bold">Save</b>.</li>
|
||||
<h3>To personalize your content</h3>
|
||||
<ol class="list">
|
||||
<li>Add a placeholder to your content by placing two brackets around the personalized elements.</li>
|
||||
<li>You can manually enter the personalized content or you can upload a spreadsheet with the details and let Notify do the
|
||||
work for you. See <a href="#prepare-data">data preparation</a>.</li>
|
||||
</ol>
|
||||
|
||||
<p>When you send a message you can either:</p>
|
||||
<h4>Example</h4>
|
||||
<p>To personalize with the recipient's first name and include a reference number:</p>
|
||||
<p class="padding-2 bg-base-lightest">State WIC: Hello ((first name)), your reference is ((ref number)). Please provide this number when you call 555-123-1234 to make an appointment.</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>manually fill in the placeholders yourself</li>
|
||||
<li>upload a list of personal details and let Notify do it for you</li>
|
||||
</ul>
|
||||
<p>Note that variations in the length of personalized content can impact the length of specific messages, and may affect
|
||||
the number of parts used.</p>
|
||||
|
||||
<p>If you upload a list, the column names need to match the placeholders in your template.</p>
|
||||
{# Add conditional content #}
|
||||
<h2 class="padding-top-1" id="conditional-content">Add conditional content</h2>
|
||||
<p>Conditional (or optional) content appears only when a recipient meets certain criteria. This feature allows you to make
|
||||
all or part of the message contingent upon specific criteria associated with the recipient.</p>
|
||||
|
||||
<h3 class="font-body-lg" id="optional-content">Add optional content</h3>
|
||||
|
||||
<p>To add optional content to your messages:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
<li>Use double brackets and ?? to define optional content. For example, if you only want to show something to people
|
||||
who are under 18: ((under18??Please get your application signed by a parent or guardian.))</li>
|
||||
<li>Select <b class="bold">Save</b>.</li>
|
||||
<h3>To add conditional content</h3>
|
||||
<ol class="list">
|
||||
<li>Use two brackets and ?? to define the conditional content.</li>
|
||||
<li>You can manually enter the conditional content or you can upload a spreadsheet with the personal details and let Notify
|
||||
do the work for you. See <a href="#prepare-data">data preparation</a>.</li>
|
||||
</ol>
|
||||
|
||||
<p>For each person you send this message to, specify ‘yes’ or ‘no’ to show or hide this content. You
|
||||
can either:</p>
|
||||
<h4>Examples</h4>
|
||||
<ol class="list">
|
||||
<li>If you only want to show something to people who are under 18:
|
||||
</br>
|
||||
<p class="padding-2 bg-base-lightest">State SNAP: Renewal applications are due by March 15. ((under18??Please get your application signed by a parent or
|
||||
guardian.))</p>
|
||||
</li>
|
||||
<li>
|
||||
If you want to make people who are homebound aware of the option of virtual visits (but not other message recipients):
|
||||
</br>
|
||||
<p class="padding-2 bg-base-lightest">State Medicaid: Please call 555-123-1234 to schedule an appointment. ((homebound??Virtual visits are available.))</p>
|
||||
</li>
|
||||
<li>
|
||||
If you want to send a messages in different languages to different recipients:
|
||||
</br>
|
||||
<p class="padding-2 bg-base-lightest">((English??We’ve identified unauthorized use on your EBT account. Call the phone number on the back of your card to
|
||||
cancel or go to your local CSO for immediate replacement.))((Spanish??Hemos identificado un uso no autorizado en su
|
||||
cuenta EBT. Llame al número de teléfono que aparece en el reverso de su tarjeta para cancelarla o diríjase a su CSO
|
||||
local para que se la sustituyan inmediatamente.))</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>do this yourself</li>
|
||||
<li>upload a list of personal details and let Notify do it for you</li>
|
||||
|
||||
{# Identify your program #}
|
||||
<h2 class="padding-top-1" id="indentify-program">Identify your program</h2>
|
||||
<p>You can help your recipients identify your texts as legitimate by customizing your messages to clearly state who they
|
||||
are from. Consider using the program or benefit name that is most familiar to your recipients.</p>
|
||||
|
||||
<h3>To customize your program name</h3>
|
||||
<p>To change the text message sender from the default service name:</p>
|
||||
<ol class="list">
|
||||
<li>Go to the <a href="{{ url_for('main.service_settings', service_id=current_service.id) }}">Settings page</a></li>
|
||||
<li>Select “Start text messages with service name.”</li>
|
||||
<li>Change the service name to a familiar program or benefit.</li>
|
||||
</ol>
|
||||
|
||||
{# Prepare your data #}
|
||||
<h2 class="padding-top-1" id="prepare-data">Prepare your data</h2>
|
||||
<p>The easiest and most efficient way to personalize your content is by uploading a spreadsheet. Notify can accommodate
|
||||
many file formats and structures.</p>
|
||||
|
||||
<h3>File format</h3>
|
||||
<p>Notify can accept files in the following formats: CSV, TSV, ODS, and Microsoft Excel.</p>
|
||||
|
||||
<h3>File structure</h3>
|
||||
<ul class="list">
|
||||
<li>The phone number must be in the first column (Column A) and must be labeled <strong>Phone number</strong>.</li>
|
||||
<li>Each column must have a unique name.</li>
|
||||
<li>If you are using Excel, you must either disable the”auto-date/time format” for time and date columns or convert the file
|
||||
to a CSV prior to loading. (If you do not, Excel will display date/time data in a confusing format.)</li>
|
||||
</ul>
|
||||
|
||||
<p>If you upload a list, the column names need to match the optional content in your template.</p>
|
||||
<h3>Formatting personalized content</h3>
|
||||
<p>If you are sending a message with personalized content, such as the first name of the recipient or the appointment time
|
||||
and location, the names of the column headings have to match the indicator included in the message template.</p>
|
||||
<p>For example, if the personalized content is the first name of the recipient, and we are using the spreadsheet below, the
|
||||
indicator in the message needs to be ((First name)), not ((firstname)) or ((name)).</p>
|
||||
|
||||
<h2 class="font-body-lg" id="customization">Message customization</h2>
|
||||
<h3>Formatting conditional content</h3>
|
||||
<p>If you are sending messages with conditional content, such as content based on the recipient’s preferred language or
|
||||
location, the flag to receive the content needs to be captured in its own column with a Yes or No (Y/N) flag.</p>
|
||||
|
||||
<h3 class="font-body-lg">Change the text message sender</h3>
|
||||
<h3>Example</h3>
|
||||
<table class="usa-table">
|
||||
<caption class="usa-sr-only">
|
||||
Example
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Phone number</th>
|
||||
<th scope="col">First name</th>
|
||||
<th scope="col">Last name</th>
|
||||
<th scope="col">Spanish</th>
|
||||
<th scope="col">English</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Lulu</td>
|
||||
<td>Praether</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>3:25</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>234-567-8901</td>
|
||||
<td>Sela</td>
|
||||
<td>Appel</td>
|
||||
<td>Y</td>
|
||||
<td>N</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>4:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Dexter</td>
|
||||
<td>Moseley</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>2:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>The text message sender tells your users who the message is from.</p>
|
||||
{# Preventing fraud #}
|
||||
<h2 class="padding-top-1" id="prevent-fraud">Preventing fraud</h2>
|
||||
<p>Texting fraud is ever prevalent and while we can’t eliminate the risk, we can reduce the chances that recipients will
|
||||
fall victim to fraud.</p>
|
||||
<ol class="list">
|
||||
<li>Never send unnecessary or protected private information.</li>
|
||||
<li>Remind your recipients that text messaging is not a secure means of communication and they should not send you private
|
||||
information via text.</li>
|
||||
<li>Only include links to websites and online applications that are secure.</li>
|
||||
<li>Send an introductory text letting recipients know you will be texting them and to save the number in their phone.</li>
|
||||
<li>Include an auto-response with the name of your organization and a contact phone number.</li>
|
||||
</ol>
|
||||
|
||||
<p>To change the text message sender from the default of ‘Notify.gov’:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the <b class="bold">Text message settings</b> section of the {{
|
||||
service_link(current_service, 'main.service_settings', 'settings') }} page.</li>
|
||||
<li>Select <b class="bold">Manage</b> on the <b class="bold">Text message
|
||||
senders</b> row.</li>
|
||||
<li>Select <b class="bold">Change</b> or <b class="bold">Add text message
|
||||
sender</b>.</li>
|
||||
<h3>Examples</h3>
|
||||
<ol class="list">
|
||||
<li>Introductory text:
|
||||
</br>
|
||||
<p class="padding-2 bg-base-lightest">State Transit Dept: We're piloting a new way to get important reminders to our staff. Save this number to your phone,
|
||||
you'll receive updates from us here.</p>
|
||||
</li>
|
||||
<li>Auto-response text:
|
||||
</br>
|
||||
<p class="padding-2 bg-base-lightest">State Agency: This number is unmonitored. To contact us, call us at 555-123-1234. We will never ask for personal details
|
||||
in a text. If you have questions about how to protect your privacy, see statename.gov/privacy.</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,26 +5,13 @@
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ job.original_file_name }}
|
||||
{{ "Message status" }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header(job.original_file_name) }}
|
||||
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
data-key="status"
|
||||
data-form=""
|
||||
>
|
||||
{% endif %}
|
||||
{{ partials['status']|safe }}
|
||||
{% if not job.processing_finished %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
{{ partials['status']|safe }}
|
||||
{% if not finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
@@ -38,6 +25,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="notification-status">
|
||||
Messages will remain in pending state until carrier status is received, typically 5 minutes.
|
||||
</p>
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<h1 class="font-body-2xl margin-bottom-3">Delivery status</h1>
|
||||
|
||||
<p>Notify’s real-time dashboard lets you check the status of any message.</p>
|
||||
<p>For <a class="usa-link" href="{{ url_for("main.security") }}">security</a>, this information is only available for seven days after a message has been sent. You can download a report, including a list of sent messages, for your own records.</p>
|
||||
<p>This page describes the statuses you’ll see when you’re signed in to Notify.</p>
|
||||
<p>For <a class="usa-link" href="{{ url_for('main.security') }}">security</a>, this information is only available for seven days after a message has been sent. You can download a report, including a list of sent messages, for your own records.</p>
|
||||
<p>This page describes the statuses you'll see when you're signed in to Notify.</p>
|
||||
|
||||
<!-- <p>If you’re using the Notify API, read our <a class="usa-link" href="{{ url_for('.documentation') }}">documentation</a> for a list of API statuses.<p>
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
caption_visible=False
|
||||
) %}
|
||||
{% for message_status, description in [
|
||||
('Total', 'The total number of messages that have been sent during the last seven days.'),
|
||||
('Pending', 'Notify has sent the message to the provider. The provider will try to deliver the message to the recipient for up to 72 hours. Notify is waiting for delivery information.'),
|
||||
('Sent', 'The mobile networks may not provide any more delivery information.'),
|
||||
('Delivered', 'The message was successfully delivered. Notify cannot tell you if a user has opened or read a message.'),
|
||||
('Not delivered', ('The provider could not deliver the message. This can happen if the phone number was wrong or if the network operator rejects the message. If you’re sure that these phone numbers are correct, you should <a class="usa-link" href="' + url_for(".support") + '">contact us</a>. If not, you should remove them from your database. You’ll still be charged for text messages that cannot be delivered.')|safe),
|
||||
('Phone not accepting messages right now', 'The provider could not deliver the message. This can happen when the recipient’s phone is off, has no signal, or their text message inbox is full. You can try to send the message again. You’ll still be charged for text messages to phones that are not accepting messages.'),
|
||||
('Technical failure', 'Your message was not sent because there was a problem between Notify and the provider. You’ll have to try sending your messages again. You will not be charged for text messages that are affected by a technical failure.'),
|
||||
] %}
|
||||
('Failed', 'The provider could not deliver the message. This can happen if the phone number was wrong or if the network operator
|
||||
rejects the message. If you’re sure that these phone numbers are correct, you should <a class="usa-link" href="/support">contact us</a>. If not, you should remove them from your database. You’ll still be charged for text messages that
|
||||
cannot be delivered.' | safe),
|
||||
] %}
|
||||
{% call row() %}
|
||||
{{ text_field(message_status) }}
|
||||
{{ text_field(description) }}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ "Error" if error else "Preview of ‘{}’".format(template.name) }}
|
||||
{{ "Error" if error else "Select delivery time" }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
@@ -40,21 +40,32 @@
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ page_header('Preview of ‘{}’'.format(template.name)) }}
|
||||
{{ page_header('Select delivery time') }}
|
||||
{% endif %}
|
||||
|
||||
{{ template|string }}
|
||||
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for(
|
||||
'main.send_notification',
|
||||
'main.preview_notification',
|
||||
service_id=current_service.id,
|
||||
template_id=template.id,
|
||||
help='3' if help else 0
|
||||
template_id=template.id
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{% if not error %}
|
||||
{% set button_text %}Send 1 {{ 1|message_count_label(template.template_type, suffix='') }}{% endset %}
|
||||
{% if choose_time_form %}
|
||||
{{ choose_time_form.scheduled_for(param_extensions={
|
||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||
'attributes': {
|
||||
'data-module': 'radio-select',
|
||||
'data-categories': choose_time_form.scheduled_for.categories|join(','),
|
||||
'data-show-now-as-default': 'true'
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% set button_text %}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
74
app/templates/views/notifications/preview.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ "Error" if error else "Preview" }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": back_link_from_preview }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
{% if error == 'not-allowed-to-send-to' %}
|
||||
<div class="bottom-gutter">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% with
|
||||
count_of_recipients=1,
|
||||
template_type_label=(
|
||||
'phone number' if template.template_type == 'sms' else 'email address'
|
||||
)
|
||||
%}
|
||||
{% include "partials/check/not-allowed-to-send-to.html" %}
|
||||
{% endwith %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% elif error == 'too-many-messages' %}
|
||||
<div class="bottom-gutter">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% include "partials/check/too-many-messages.html" %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% elif error == 'message-too-long' %}
|
||||
{# the only row_errors we can get when sending one off messages is that the message is too long #}
|
||||
<div class="bottom-gutter">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% include "partials/check/message-too-long.html" %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% else %}
|
||||
{{ page_header('Preview') }}
|
||||
{% endif %}
|
||||
<div>
|
||||
<p class="sms-message-scheduler">Scheduled: {{ scheduled_for |format_datetime_normal if scheduled_for else 'Now'}}</p>
|
||||
<p class="sms-message-template">Template: {{template.name}}</p>
|
||||
<p class="sms-message-sender" >From: {{ template.sender }}</p>
|
||||
<p class="sms-message-sender" >To: {{ recipient }}</p>
|
||||
</div>
|
||||
|
||||
<h2 id="{{ file_contents_header_id }}">Message</h2>
|
||||
<div class="preview-message"> {{ template|string }}</div>
|
||||
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for(
|
||||
'main.send_notification',
|
||||
service_id=current_service.id,
|
||||
template_id=template.id,
|
||||
help='3' if help else 0
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<!-- <p>Placeholder: This message will be delivered to <b>400 phone numbers</b> and will use a total of <b>800 message parts</b>, leaving Washington DSHS with <b>249,200 message parts remaining</b>.</p> -->
|
||||
<h3>Does everything look good?</h3>
|
||||
{% if not error %}
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -44,31 +44,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
notifications_by_type|reverse,
|
||||
caption='Messages sent since May 2023',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Date',
|
||||
99|message_count_noun('email')|capitalize,
|
||||
99|message_count_noun('sms')|capitalize,
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.date | format_date_normal }}
|
||||
<div class="table-wrapper">
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
notifications_by_type|reverse,
|
||||
caption='Messages sent since May 2023',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Date',
|
||||
99|message_count_noun('email')|capitalize,
|
||||
99|message_count_noun('sms')|capitalize,
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.date | format_date_normal }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.emails|format_thousands }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.sms|format_thousands }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.emails|format_thousands }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.sms|format_thousands }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the last {{ notifications_by_type|length }} days
|
||||
</p>
|
||||
<p class="table-show-more-link">
|
||||
Only showing the last {{ notifications_by_type|length }} days
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="govuk-heading-m">
|
||||
@@ -82,26 +84,28 @@
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
processing_time | reverse,
|
||||
caption='Messages sent within 10 seconds',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Date', 'Percentage'
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.date | format_date_normal }}
|
||||
<div class="table-wrapper">
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
processing_time | reverse,
|
||||
caption='Messages sent within 10 seconds',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Date', 'Percentage'
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.date | format_date_normal }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ '{:.2f}%'.format(item.percentage_under_10_seconds) }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ '{:.2f}%'.format(item.percentage_under_10_seconds) }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
<p class="table-show-more-link">
|
||||
Only showing the last {{ processing_time|length }} days
|
||||
</p>
|
||||
<p class="table-show-more-link">
|
||||
Only showing the last {{ processing_time|length }} days
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="govuk-heading-m">
|
||||
@@ -120,23 +124,25 @@
|
||||
<span class="usa-sr-only">using Notify.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
organizations_using_notify,
|
||||
caption='Organizations using Notify',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Organization', 'Number of live services'
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.organization_name }}
|
||||
<div class="table-wrapper">
|
||||
<div class="dashboard-table">
|
||||
{% call(item, row_number) list_table(
|
||||
organizations_using_notify,
|
||||
caption='Organizations using Notify',
|
||||
caption_visible=False,
|
||||
field_headings=[
|
||||
'Organization', 'Number of live services'
|
||||
],
|
||||
empty_message='No data to show'
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.organization_name }}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.count_of_live_services }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{{ item.count_of_live_services }}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
{{ text_field(item.complaint_type) }}
|
||||
|
||||
{{ text_field(item.complaint_date|format_datetime_short if item.complaint_date else None) }}
|
||||
{{ text_field(item.complaint_date|format_datetime_table if item.complaint_date else None) }}
|
||||
|
||||
{% endcall %}
|
||||
|
||||
|
||||