Compare commits

...

83 Commits

Author SHA1 Message Date
Carlo Costino
ad34ca0a11 Merge pull request #1192 from GSA/govuk_frontend_jinja
inline code for govuk_frontend_jinja
2024-02-06 16:48:23 -05:00
Carlo Costino
df54b066f9 Update notifications-utils to 0.2.8
This updates utils to be 0.2.8 to address the cryptography Dependabot security alert.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-02-06 16:41:07 -05:00
Kenneth Kehl
338a6426c3 inline code for govuk_frontend_jinja 2024-02-06 13:17:09 -08:00
Carlo Costino
f37903ff6d Merge pull request #1183 from GSA/notify-admin-1159
Notify admin 1159 -- update to use sent_at instead of updated_at
2024-02-05 17:20:53 -05:00
Kenneth Kehl
802ebfa243 use sent_at instead of updated_at 2024-02-02 08:31:05 -08:00
Tim Lowden
e18054d2d8 Merge pull request #1160 from GSA/449-one-pager-content-updates
449 one pager content updates
2024-02-01 09:15:57 -05:00
Kenneth Kehl
21c11adb5e fix sent time 2024-01-31 10:27:56 -08:00
Carlo Costino
266b530e3a Merge pull request #1175 from GSA/add-login-gov-env-vars
Add Login.gov configuration to prod and demo environments
2024-01-30 17:26:34 -05:00
Carlo Costino
6af738e0de Add Login.gov configuration to prod and demo environments
This changeset adds the required Login.gov environment variables to the production and demo environments so that deployments function correctly.

NOTE:  This will not fully enable Login.gov integration just yet; more configuration needs to be adjusted and the Login.gov app needs to be promoted to production itself.
Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-30 17:21:29 -05:00
Carlo Costino
27385bc45a Merge pull request #1170 from GSA/fix-infrastructure-checks
Fix Terraform commands and actions
2024-01-30 15:04:35 -05:00
Carlo Costino
f097b9ca07 Merge pull request #1167 from GSA/notify-admin-1148
notify-admin-1148 make sure one-off files get written to db
2024-01-30 11:38:58 -05:00
Carlo Costino
50e3560c6f Remove the profile attribute entirely
This was what was apparently breaking our Terraform actions.  I traced this to an undocumented breaking change with the AWS provider; more details can be seen here: https://discuss.hashicorp.com/t/error-error-configuring-terraform-aws-provider-failed-to-get-shared-config-profile-default/39417/2

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-30 11:06:13 -05:00
Jonathan Bobel
761c7bd0fd Updated security content page 2024-01-30 09:49:04 -05:00
Carlo Costino
4da5c14751 Updated 18F/terraform-cloudgov to v0.7.1
Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-30 09:40:14 -05:00
Carlo Costino
0f167a91cc Update Cloud Foundry Terraform module
This changeset updates the Cloud Foundry Terraform to its latest release.  Our file were previously referencing a very old version, which was contributing to infrastructure check and deployment failures.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-30 09:26:04 -05:00
Kenneth Kehl
7c5674c4e1 notify-admin-1137 make sure one-off files get written to db 2024-01-29 13:17:50 -08:00
Jonathan Bobel
6d3c92de28 Merge branch 'main' of https://github.com/GSA/notifications-admin into 449-one-pager-content-updates 2024-01-29 10:32:17 -05:00
Jonathan Bobel
e40a493f9b Updated Roadmap content 2024-01-29 10:28:59 -05:00
Carlo Costino
4e488fa079 Merge pull request #1162 from GSA/1161-bug-fix-ui-time-sent-column-to-match-time-the-message-was-created_at
BUG: Fix UI Time Sent column to match time the message was created_at
2024-01-26 16:48:40 -05:00
Beverly Nguyen
5d480b3f53 updated time 2024-01-26 13:37:49 -08:00
Jonathan Bobel
070955175d Merge branch 'main' of https://github.com/GSA/notifications-admin into 449-one-pager-content-updates 2024-01-26 15:22:30 -05:00
Carlo Costino
a5eb311f83 Merge pull request #1129 from GSA/1114-reviseexpand-on-recent-batch-table-on-the-dashboard-page
Revise/expand on "Recent Batch" table on the dashboard page.
2024-01-26 15:03:08 -05:00
Beverly Nguyen
6b4a5d009c removed comment 2024-01-26 11:51:52 -08:00
Beverly Nguyen
1f12dd3908 show only the first 5 rows 2024-01-26 11:47:46 -08:00
Jonathan Bobel
0e64ff1c20 Contact us page 2024-01-26 14:34:25 -05:00
Beverly Nguyen
f7924320df adjusted styling 2024-01-26 11:32:42 -08:00
Beverly Nguyen
1b4754386c update testing for signout and accept invite 2024-01-26 11:29:03 -08:00
Jonathan Bobel
a27519ff68 Removed white space 2024-01-26 14:20:47 -05:00
Jonathan Bobel
b72430af63 Guidance page 2024-01-26 14:20:23 -05:00
Beverly Nguyen
35b38f838c fixed testing for dashboard.py 2024-01-26 11:16:45 -08:00
Beverly Nguyen
45ffa70c3c fixing mocker patch for jobs 2024-01-26 09:43:41 -08:00
Jonathan Bobel
bd33fe4750 Merge branch 'main' of https://github.com/GSA/notifications-admin into 449-one-pager-content-updates 2024-01-26 10:20:37 -05:00
Jonathan Bobel
768f1986e8 449 - Content updates
- Get started
2024-01-26 10:10:49 -05:00
Carlo Costino
e48cb94ccb Merge pull request #1143 from GSA/963-message-parts-content-updates
963 message parts content updates
2024-01-26 09:40:17 -05:00
Carlo Costino
0fc14131b1 Merge pull request #1147 from GSA/dependabot/pip/newrelic-9.6.0
Bump newrelic from 9.5.0 to 9.6.0
2024-01-26 09:28:38 -05:00
Beverly Nguyen
6e05fd5147 removed comments 2024-01-25 15:39:32 -08:00
dependabot[bot]
c79c824b24 Bump newrelic from 9.5.0 to 9.6.0
Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 9.5.0 to 9.6.0.
- [Release notes](https://github.com/newrelic/newrelic-python-agent/releases)
- [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v9.5.0...v9.6.0)

---
updated-dependencies:
- dependency-name: newrelic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-25 23:32:58 +00:00
Beverly Nguyen
524a1e4687 updated download links and table html 2024-01-25 15:32:44 -08:00
Carlo Costino
9513069b42 Merge pull request #1146 from GSA/tdlowden-patch-1
Create manual_qa_template.yml
2024-01-25 16:16:49 -05:00
Tim Lowden
af8309a2ce Update manual_qa_template.yml 2024-01-25 16:07:58 -05:00
Tim Lowden
37086cb03b Update manual_qa_template.yml 2024-01-25 15:57:29 -05:00
Tim Lowden
3ac9d44b2a Update manual_qa_template.yml 2024-01-25 15:56:28 -05:00
Tim Lowden
d5210427f0 Create manual_qa_template.yml
Just starting this
2024-01-25 15:52:59 -05:00
Jonathan Bobel
da1e2a2700 Merge branch 'main' of https://github.com/GSA/notifications-admin into 963-message-parts-content-updates 2024-01-25 14:55:22 -05:00
Jonathan Bobel
d9875ef287 Content updates for Pricing, Trial Mode, and various areas of changing "message parts" 2024-01-25 14:55:17 -05:00
Steven Reilly
cdd262365f Merge pull request #1141 from GSA/update-utils-0-2-7
Update notifications-utils to 0.2.7
2024-01-24 16:05:48 -05:00
Carlo Costino
195c68037b Merge pull request #1140 from GSA/notify-admin-1090
change download reports to 12 hour time (notify-admin-1090)
2024-01-24 15:53:22 -05:00
Carlo Costino
d5daab67cf Fixes tests to match recent notifications-utils updates
Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-24 15:35:09 -05:00
Jonathan Bobel
782ec1c5eb 963 - content updates 2024-01-24 15:25:19 -05:00
Kenneth Kehl
fd8981d5cd add timezone to file name 2024-01-24 11:59:31 -08:00
Carlo Costino
1709d3279b Update notifications-utils to 0.2.7
This changeset updates the notifications-utils library to 0.2.7.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-24 14:55:55 -05:00
Kenneth Kehl
2126ba8038 fix name of download file 2024-01-24 11:54:25 -08:00
Beverly Nguyen
c9ed1ab59d change time column 2024-01-24 11:15:35 -08:00
Kenneth Kehl
a38518777c change download reports to 12 hour time 2024-01-24 11:03:19 -08:00
Carlo Costino
3379259e14 Merge pull request #1136 from GSA/dependabot/pip/python-dotenv-1.0.1
Bump python-dotenv from 1.0.0 to 1.0.1
2024-01-24 13:55:52 -05:00
dependabot[bot]
ede82d76d6 Bump python-dotenv from 1.0.0 to 1.0.1
Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.0.0 to 1.0.1.
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1)

---
updated-dependencies:
- dependency-name: python-dotenv
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-24 18:34:27 +00:00
Carlo Costino
5970dd8055 Merge pull request #1135 from GSA/dependabot/pip/bandit-1.7.7
Bump bandit from 1.7.6 to 1.7.7
2024-01-24 13:31:53 -05:00
Beverly Nguyen
65bccb3f95 changed time column 2024-01-23 16:18:43 -08:00
dependabot[bot]
75a4e1ae6d Bump bandit from 1.7.6 to 1.7.7
Bumps [bandit](https://github.com/PyCQA/bandit) from 1.7.6 to 1.7.7.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.7.6...1.7.7)

---
updated-dependencies:
- dependency-name: bandit
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 23:36:48 +00:00
Beverly Nguyen
8baf9682a1 update table css and format 2024-01-23 15:24:57 -08:00
Steven Reilly
6b00cf1270 Merge pull request #1133 from GSA/set-no-proxy-exclusion
Add no_proxy environment variable
2024-01-22 16:27:45 -05:00
Carlo Costino
490174ab71 Add no_proxy environment variable
This changeset adds the no_proxy environment variable to be set at the time our app runs to exclude the internal apps running within our platform that are handled by Cloud Foundry network policies instead.  Without this in place, the egress proxy will not allow our deployed apps to talk with one another.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-22 15:36:00 -05:00
Carlo Costino
f15643c04d Merge pull request #1130 from GSA/adjust-egress-proxy-config
Add missing egress proxy configuration
2024-01-19 17:19:09 -05:00
Carlo Costino
d253b601a2 Add missing egress proxy configuration
This changeset adds some egress proxy configuration that is currently missing from the application initialization.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-01-19 17:14:11 -05:00
Beverly Nguyen
9b0dc3dc3c updated table titles and fields 2024-01-19 11:44:25 -08:00
Carlo Costino
443c7d9992 Merge pull request #1106 from GSA/tdlowden-patch-1
Update sprint-goals.md
2024-01-18 16:27:44 -05:00
Tim Lowden
33433a3822 Update sprint-goals.md 2024-01-18 12:45:03 -05:00
Tim Lowden
c7a393e620 Update sprint-goals.md 2024-01-18 12:43:55 -05:00
Tim Lowden
ba3a229d60 Update sprint-goals.md 2024-01-18 12:42:33 -05:00
Jonathan Bobel
17b1352f01 Merge pull request #1121 from GSA/tdlowden-patch-3
Update bug_template.yml
2024-01-18 11:38:32 -05:00
Tim Lowden
a1440ae36f Update bug_template.yml 2024-01-18 11:33:20 -05:00
Tim Lowden
d49fb8867e Update bug_template.yml 2024-01-18 11:30:43 -05:00
Carlo Costino
98b11dc3c4 Merge pull request #1103 from GSA/818-allow-single-messages-to-be-scheduled
Allow single messages to be scheduled #818
2024-01-18 11:07:00 -05:00
Carlo Costino
6fe29a2bdd Merge pull request #1101 from GSA/1089-change-team-member-card-width
1089 change team member card width
2024-01-18 10:30:00 -05:00
Tim Lowden
b463d420ef Merge pull request #1119 from GSA/fixing-the-bug-template
Replicated the User Story Template in a few ways to see if that fixes…
2024-01-18 10:28:04 -05:00
Carlo Costino
2a9f60c32e Merge pull request #1100 from GSA/1022-sentence-about-5-minutes-til-message-receipts-doesnt-appear
Sentence about 5 minutes til message receipts doesn't appear #1022
2024-01-18 10:19:14 -05:00
Tim Lowden
adda14342b Update sprint-goals.md 2024-01-17 10:07:55 -05:00
Beverly Nguyen
d3c3183573 added choose time form 2024-01-16 15:37:56 -08:00
Beverly Nguyen
25305d0706 update testing 2024-01-16 12:01:39 -08:00
Beverly Nguyen
0e110201d6 update testing 2024-01-16 11:28:58 -08:00
Beverly Nguyen
11a85d688a added notifications status 2024-01-15 14:17:18 -08:00
Jonathan Bobel
82863b02ce Merge branch 'main' of https://github.com/GSA/notifications-admin into 1089-change-team-member-card-width 2024-01-11 16:00:04 -05:00
Jonathan Bobel
27c74ca773 1089- styling update to Team Member cards 2024-01-11 15:56:17 -05:00
52 changed files with 1242 additions and 788 deletions

View File

@@ -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

View 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 persons 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

View File

@@ -55,6 +55,13 @@ jobs:
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
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-demo.app.cloud.gov/sign-out"
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://idp.int.identitysandbox.gov/openid_connect/logout?"
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-demo.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-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=abcdefghijklmnopabcdefghijklmnop"
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
@@ -69,6 +76,13 @@ jobs:
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
--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

View File

@@ -55,7 +55,13 @@ jobs:
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
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=01234567890123456789012345&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=abcdefghijklmnopabcdefghijklmnop"
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
@@ -70,6 +76,13 @@ jobs:
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
--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

View File

@@ -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"

View File

@@ -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
@@ -54,6 +53,7 @@ from app.formatters import (
format_datetime_normal,
format_datetime_relative,
format_datetime_short,
format_datetime_short_america,
format_day_of_week,
format_delta,
format_delta_days,
@@ -113,6 +113,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()
@@ -548,6 +549,7 @@ def add_template_filters(application):
format_datetime_24h,
format_datetime_normal,
format_datetime_short,
format_datetime_short_america,
valid_phone_number,
linkable_name,
format_date,

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -124,6 +124,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;
@@ -141,10 +142,11 @@ td.table-empty-message {
.user-list {
display: inline-flex;
flex-wrap: wrap;
gap: units(2);
gap: units(4);
.user-list-item {
@include at-media(desktop) {
flex: 1 1 calc(50% - units(2));
flex-grow: 0;
}
flex: 1 1 calc(100% - units(2));
border: 1px solid color('gray-cool-10');
@@ -375,6 +377,32 @@ td.table-empty-message {
}
}
.job-table {
.usa-table {
thead th.file-name {
width: 25%;
}
thead th.template {
width: 20%;
}
thead th.time-sent {
width: 30%;
}
thead th.sender {
width: 15%;
}
thead th.\#-of-recipients {
width: 5%;
}
thead th.report {
width: 5%;
}
th {
padding: 0.5rem 0.5rem;
}
}
}
#template-list {
max-height: 500px;
overflow-y: auto;

View File

@@ -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 youre 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>. Well 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 youre ready to send messages to people outside your team. Well 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).
Well 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 youre 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)

View File

@@ -98,6 +98,28 @@ def format_datetime_short(date):
)
def format_datetime_short_america(date):
return "{} at {}".format(format_date_numeric_america(date), format_time_12h(date))
def format_date_numeric_america(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
)
def format_time_12h(date):
date = parse_naive_dt(date)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
)
def format_datetime_relative(date):
return "{} at {} {}".format(
get_human_day(date), format_time_24h(date), get_user_preferred_timezone()
@@ -448,6 +470,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"

View File

@@ -47,54 +47,31 @@ 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,
)["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)
job_response = job_api_client.get_jobs(service_id)
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"],
jobs = [
{
"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,
}
)
"notification_count": job["notification_count"],
"created_by": job["created_by"],
}
for job in job_response["data"]
]
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,
jobs=jobs,
service_data_retention_days=service_data_retention_days,
)

View File

@@ -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,
)

View File

@@ -780,6 +780,8 @@ def _check_notification(service_id, template_id, exception=None):
back_link = get_back_link(service_id, template, len(placeholders), placeholders)
choose_time_form = ChooseTimeForm()
if (not session.get("recipient")) or not all_placeholders_in_session(
template.placeholders
):
@@ -791,6 +793,7 @@ def _check_notification(service_id, template_id, exception=None):
return dict(
template=template,
back_link=back_link,
choose_time_form=choose_time_form,
**(get_template_error_dict(exception) if exception else {}),
)
@@ -861,7 +864,7 @@ def send_notification(service_id, template_id):
job_api_client.create_job(
upload_id,
service_id,
scheduled_for="",
scheduled_for=request.form.get("scheduled_for", ""),
template_id=template_id,
original_file_name=filename,
notification_count=1,
@@ -877,14 +880,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(

View File

@@ -172,7 +172,7 @@
<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.sent_at or notification.created_at)|format_datetime_short
) }}
</p>
{% if displayed_on_single_line %}</span>{% endif %}

View File

@@ -29,51 +29,69 @@
{{ ajax_block(partials, updates_url, 'template-statistics') }}
<h2 class="margin-top-4 margin-bottom-1">Batched Jobs</h2>
<h2 class="margin-top-4 margin-bottom-1">Recent Batches</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 &thinsp;(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>
{% endif %}
{% endcall %}
{% endif %}
{% endcall %}
<table class="usa-table usa-table--borderless width-full">
<thead class="table-field-headings">
<tr>
<th scope="col" class="table-field-heading-first file-name">
<span>File name</span>
</th>
<th scope="col" class="table-field-heading template">
<span>Template</span>
</th>
<th scope="col" class="table-field-heading time-sent">
<span>Time sent</span>
</th>
<th scope="col" class="table-field-heading sender">
<span>Sender</span>
</th>
<th scope="col" class="table-field-heading #-of-recipients">
<span># of Recipients</span>
</th>
<th scope="col" class="table-field-heading report">
<span>Report</span>
</th>
</tr>
</thead>
<tbody>
{% for notification in notifications[:5] %}
{% if notification %}
<tr class="table-row" id="{{ notification.job.id }}">
<th class="table-field">
{{ notification.job.original_file_name if notification.job.original_file_name else 'Manually entered number'}}
<br>
<a class="usa-link file-list-filename" href="/services/{{ notification.service }}/jobs/{{ notification.job.id }}">View Batch</a>
</th>
<th class="table-field">
{{ notification.template.name }}
</th>
<th class="table-field">
{{ notification.created_at | format_datetime_short_america }}
</th>
<th class="table-field">
{{ notification.created_by.name }}
</th>
{% set job_available = jobs|selectattr('job_id', 'equalto', notification.job.id)|first %}
<th class="table-field">
{{ job_available.notification_count if job_available else ''}}
</th>
<th class="table-field">
{% if job_available and job_available.time_left != "Data no longer available" %}
<a class="usa-link file-list-filename" href="{{ job_available.download_link }}">
{{ "Download" if job_available.job_id else '' }}</a>
<span class="usa-hint">{{ job_available.time_left }}</span>
{% elif job_available %}
<span>{{ job_available.time_left }}</span>
{% endif %}
</th>
</tr>
{% endif %}
{% endfor %}
</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>

View File

@@ -11,115 +11,229 @@
<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 “Send Messages”. Youll 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 theres 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 theres 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 persons 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&nbsp;name)), your reference is
((ref&nbsp;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 123-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>
{# 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>
<p>If you upload a list, the column names need to match the placeholders in your template.</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 123-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??Weve 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="identify-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 recipients 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 cant 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 123-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 %}

View File

@@ -38,6 +38,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"

View File

@@ -54,6 +54,16 @@
)}}" class='page-footer'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
{% if not error %}
{% 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 %}Send 1 {{ 1|message_count_label(template.template_type, suffix='') }}{% endset %}
{{ usaButton({ "text": button_text }) }}
{% endif %}

View File

@@ -15,62 +15,20 @@ Pricing
{{ content_metadata(
data={
"Last updated": "Feb 27, 2023"
"Last updated": "January 25, 2024"
}
) }}
<p>During pilot period, each service has an allowance of 250,000 free text messages. 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>When a new service is added, it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>. Moving a service out of trial mode is subject to approval by
the Notify.gov team.</p>
<p>You'll use more messages towards the allowance if you send text messages longer than 160 characters.
</p>
<p>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. You'll use
more parts towards the allowance if you:</p>
<p>
{% if not current_user.is_authenticated %}
<a class="usa-link" href="{{ url_for('main.register') }}">Create an account,</a> then
add your unique service(s).
</p>
{% else %}
Additional unique services may be added, although moving each service out of trial mode is subject to approval by the
Notify.gov team.
{% endif %}
</p>
<p>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>
<p>Youll use more messages towards the allowance if you send text messages longer than 160 characters.</p>
<!-- <h2 class="font-body-lg" id="emails">Emails</h2>
<p>Its free to send emails through Notify.</p> -->
<h2 class="font-body-lg" id="text-messages">Text messages</h2>
<p>During pilot phase, each service has an annual allowance of 250,000 free text messages. Once this allowance is met,
the application will stop delivering messages.</p>
<!-- <p>When a service has used its annual allowance, it costs {{ sms_rate }} pence (plus VAT) for each text message you send.</p> -->
<p>Youll use more messages towards the allowance, if you send <a class="usa-link"
href="#long-text-messages">text messages longer than 160 characters</a>.</p>
<p>You may also use more messages if you:</p>
<ul class="list list-bullet">
<li>send text messages longer than 160 characters</a></li>
<li>use certain <a class="usa-link" href="#symbols">signs and symbols</a></li>
<li>use <a class="usa-link" href="#accents">accents and accented letters</a></li>
<li>send text messages to <a class="usa-link"
href="#international-numbers">international numbers</a></li>
</ul>
<!-- <p>See <a class="usa-link" href="{{ url_for('main.how_to_pay') }}">how to pay</a>.
<h3 class="font-body-lg" id="free-text-message-allowance">Free text message allowance</h3>
<p>The current allowance is:</p>
<ul class="list list-bullet">
<li>40,000 free text messages for national services</li>
<li>20,000 free text messages for regional services</li>
<li>10,000 free text messages for state-funded schools and GP practices</li>
</ul>
<p>Each unique service you add has a separate allowance.</p> -->
<h3 class="font-body-lg" id="long-text-messages">Long text messages</h3>
<p>If a text message is longer than 160 characters (including spaces), it counts as more than one message.</p>
@@ -82,12 +40,12 @@ Notify.gov team.
caption_visible=False
) %}
{% for message_length, charge in [
('Up to 160 characters', '1 text message'),
('Up to 306 characters', '2 text messages'),
('Up to 459 characters', '3 text messages'),
('Up to 612 characters', '4 text messages'),
('Up to 765 characters', '5 text messages'),
('Up to 918 characters', '6 text messages'),
('Up to 160 characters', '1 part'),
('Up to 306 characters', '2 parts'),
('Up to 459 characters', '3 parts'),
('Up to 612 characters', '4 parts'),
('Up to 765 characters', '5 parts'),
('Up to 918 characters', '6 parts'),
] %}
{% call row() %}
{{ text_field(message_length) }}
@@ -186,11 +144,11 @@ Notify.gov team.
caption_visible=False
) %}
{% for message_length, charge in [
('Up to 70 characters', '1 text message'),
('Up to 134 characters', '2 text messages'),
('Up to 201 characters', '3 text messages'),
('Up to 268 characters', '4 text messages'),
('Each additional 67 characters', '1 additional text message'),
('Up to 70 characters', '1 part'),
('Up to 134 characters', '2 parts'),
('Up to 201 characters', '3 parts'),
('Up to 268 characters', '4 parts'),
('Each additional 67 characters', '1 additional part'),
] %}
{% call row() %}
{{ text_field(message_length) }}
@@ -200,43 +158,9 @@ Notify.gov team.
{% endcall %}
</div>
<h3 class="font-body-lg" id="international-numbers">Sending text messages to international numbers</h3>
<p>It might cost more to send text messages to international numbers than U.S. ones, depending on the country.</p>
<!-- {% set smsIntRates %}
{{ live_search(target_selector='#international-pricing .table-row', show=True, form=search_form, label='Search by country name or code') }}
<h3 class="font-body-lg" id="international-numbers">Trial mode</h3>
<p>When a new service is added, it will start in <a href="/using-notify/trial-mode">trial mode</a>. Moving a service out of trial mode is subject to approval by
the Notify.gov team. Additional unique services may be added, although moving each service out of trial mode is subject
to approval by the Notify.gov team.</p>
<div id="international-pricing" class="bottom-gutter-3-2">
{% call mapping_table(
caption='International text message rates',
field_headings=['Country code', 'Country', 'Cost multiplier'],
field_headings_visible=True,
caption_visible=False
) %}
{% for cc, names, billable_units in international_sms_rates %}
{% call row() %}
{{ text_field('+' + cc) }}
{% call field() %}
{% for name in names %}
{{ name }}<br>
{% endfor %}
{% endcall %}
{{ text_field('{}&hairsp;&times;'.format(billable_units)|safe) }}
{% endcall %}
{% endfor %}
{% call row() %}
{{ text_field('+882') }}
{% call field() %}Worldwide{% endcall %}
{{ text_field('Not supported') }}
{% endcall %}
{% endcall %}
</div>
{% endset %}
{{ usaDetails({
"summaryText": "International text message rates",
"id": "sms-pricing-details",
"html": smsIntRates
}) }} -->
{% endblock %}
{% endblock %}

View File

@@ -22,18 +22,22 @@
<p>This roadmap is only a guide. It does not cover everything we do, and some things may change.</p>
<p>You can <a class="usa-link" href="{{url_for('.support')}}">contact us</a> if you have any questions about the roadmap or suggestions for new features.</p>
<h2 class="font-body-lg" id="things-we-are-working-on">What were working on</h2>
<h2 id="things-we-are-working-on">What were working on</h2>
<h3 class="font-body-lg" id="now">Now</h3>
<h3 id="now">Now</h3>
<p>We are investigating the Notify concept, building on the notifications tool pioneered by the UK.</p>
<p>To do this, we are convening a pilot with a small set of partners.</p>
<p>Goals during this stage:</p>
<ul class="list list-bullet">
<li>Achieve compliance to begin piloting, such as ATO and privacy standards</li>
<li>Demonstrate that a government-run notifications tool provides a unique value</li>
<li>Gather data from the pilot to improve the product</li>
<li>Achieve compliance to begin piloting, such as ATO and privacy standards.
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
use xlink:href="/assets/img/sprite.svg#check"></use>
</svg>
</li>
<li>Demonstrate that a government-run notifications tool provides a unique value.</li>
<li>Gather data from the pilot to improve the product.</li>
</ul>
<p>Features prioritized during this stage:</p>
@@ -46,53 +50,38 @@
<li>Message send/failure analytics</li>
</ul>
<h3 class="font-body-lg" id="next">Next</h3>
<h3id="next">Next</h3>
<p>If the pilot is successful, we hope to recruit additional high-impact partners to improve outcomes for low-income individuals and families.</p>
<p>Goals during this stage:</p>
<ul class="list list-bullet">
<li>Complement Notify with practical guidance and support services</li>
<li>Iterate on existing features and implement new features based on what we've learned so far</li>
<li>Hone our measurement approaches to better quantify impact</li>
<li>Complement Notify with practical guidance and support services.</li>
<li>Iterate on existing features and implement new features based on what we've learned so far.</li>
<li>Hone our measurement approaches to better quantify impact.</li>
</ul>
<p>Features prioritized during this stage:</p>
<ul class="list list-bullet">
<ul class="list">
<li>SMS sending via API integration</li>
<li>Single-level decision two-way messaging (e.g. reply “YES” if, or “NO” if…)</li>
<li>Self-service account creation</li>
<li>Application status page</li>
<li>Scheduled send option</li>
<li>Improved scheduled send option</li>
</ul>
<h3 class="font-body-lg" id="later">Later</h3>
<h3 id="later">Later</h3>
<p>In the future, we may decide to expand beyond SMS, or to offer the service government-wide.</p>
<p>Features to be considered during this stage:</p>
<ul class="list list-bullet">
<ul class="list">
<li>Email sending via UI and API</li>
<li>Multiple-level decision two-way messaging (greater than one layer decision-tree)</li>
<li>Open-text reply two-way messaging (rather than reply yes or no, 1 or 2, etc.)</li>
<li>Two-way messaging</li>
<li>Multilingual interface and content library options</li>
<li>Recurring scheduled send (such as “Send each Monday for 3 weeks”)</li>
</ul>
<!-- <h2 class="font-body-lg" id="things-we-have-done">Things weve done</h2>
<p>Recently we have:</p>
<ul class="list list-bullet">
<li></li>
<li></li>
</ul> -->
{% endblock %}

View File

@@ -15,22 +15,52 @@
<li>manage risks around information</li>
</ul>
<h2 class="font-body-lg" id="data">Data</h2>
<p>On Notify, data is encrypted:</p>
<ul class="list list-bullet">
<li>when it passes through the service</li>
<li>when its stored on the service</li>
</ul>
<p>Any recipient data you upload is only held for seven days.</p>
<!-- <p>If you <a class="usa-link" href="/features/email#send-files">send a file by email</a>, the file will be available for the recipient to download for 18 months.</p> -->
<h2 id="infrastructure">Infrastructure</h2>
<h2 class="font-body-lg" id="technical-security">Technical security</h2>
<!-- <p>Other technical security controls on Notify include:</p>
<ul class="list list-bullet">
<li>compliance with National Cyber Security Centre (NCSC) Cloud Security Principles</li>
<li>protective monitoring to record activity, and raise alerts about any suspicious activity</li>
<li>using JSON Web Tokens, to avoid sending API keys when your service talks to Notify</li>
</ul> -->
<p>Notify.gov is comprised of two applications both running on <a href="https://www.cloud.gov">cloud.gov</a>:</p>
<ul class="list">
<li>Admin, a Flask website running on the python_buildpack which hosts agency user-facing UI</li>
<li>API, a Flask application running on the python_buildpack hosting the Notify API</li>
</ul>
<p>Notify.gov utilizes several cloud.gov-provided services through Amazon Web Services (AWS):</p>
<ul class="list">
<li>AWS S3 buckets for temporary file storage</li>
<li>AWS Elasticache (Redis) for caching data and enqueueing background tasks</li>
<li>AWS RDS (PostgreSQL) for system data storage</li>
</ul>
<p>Notify.gov also provisions and uses two AWS services via a <a href="https://github.com/GSA/datagov-ssb">supplemental service broker</a>:</p>
<ul class="list">
<li><a href="https://aws.amazon.com/sns/">AWS SNS</a> for sending SMS messages</li>
<li><a href="https://aws.amazon.com/ses/">AWS SES</a> for sending email messages (note: email function is currently unavailable to users for pilot)</li>
</ul>
<h2 id="current-security-review">Current security review</h2>
<p>Currently, Notify.gov operates under a GSA <a href="https://www.gsa.gov/system/files/Lightweight-Security-Authorization-Process-%28LATO%29%20%5BCIO-IT-Security-14-68-Rev-7%5D%2009-17-2021docx%20%281%29.pdf">Lightweight Authority to Operate (LATO)</a>. This federal security authorization
process leverages security controls provided by National Institute of Standards and Technology (NIST). The process is
focused on operational security from both a functional and assurance perspective.</p>
<p>We are pursuing a full Authority to Operate (ATO)</p>
<h2 id="data">Data</h2>
<p>To send a message, agencies upload a spreadsheet of phone numbers and other necessary data from their existing data
management system. On Notify.gov, data is encrypted when it passes through the service and when its stored on the
service.</p>
<p>Notify.gov is not a system of record and as a result does not have a SORN. Agencies are responsible for managing their
data outside of Notify.gov.</p>
<ul class="list">
<li>Notify stores data within the cloud.gov-managed PostgreSQL database and <a href="https://cloud.gov/docs/services/s3/#encryption">S3 buckets</a>. The entire database is <a href="https://cloud.gov/docs/services/relational-database/#encryption">encrypted at
the disk level</a>.</li>
<li>Additionally, data is encrypted at the field level when it is stored in the database, using OpenSSL 3.0.8 via the Python
cryptography library. The implementation is available <a href="https://github.com/GSA/notifications-utils/blob/main/notifications_utils/clients/encryption/encryption_client.py">on GitHub</a>.</li>
</ul>
<h2 id="data-retention">Data retention</h2>
<p>Any recipient data uploaded is only held for seven days; all personally identifiable information (PII) is deleted for
successful messages, so data is retained only for unsuccessful messages.
</p>
<h2 id="technical-security">Technical security</h2>
<h3 class="font-body-lg">Protect sensitive information</h3>
<p>Some messages include sensitive information like security codes or password reset links.</p>
@@ -46,6 +76,23 @@
</ul>
<p>If signing in with a text message is a problem for your team, <a class="usa-link" href="{{ url_for('main.support') }}">contact us</a> to find out about using an email link instead.</p>
<img src="{{ asset_url('images/product/security-review-message.png') }}"
alt="Screenshot of a teat message in review with the link to 'hide personalization after sending' circled.">
<h4>How to hide PII after sending a message</h4>
<h3>User permissions and signing in</h3>
<p>You can set different user permissions in Notify. This lets you control who in your team has access to certain parts of
the service.</p>
<h3>Multi-factor authentication (MFA)</h3>
<p>Notify.gov uses <a href="https://login.gov/">Login.gov</a> to authenticate users.</p>
<ul class="list">
<li>Your email address and password</li>
<li>A code from an authenticator app or sent by Login.gov in the form of a text message</li>
</ul>
<p>If signing in with a text message is a problem for your team, <a href="https://beta.notify.gov/support">contact us</a> to find out about using an email link instead.</p>
<!-- <h2 class="font-body-lg" id="information-risk-management">Information risk management</h2>
<p>Our approach to information risk management follows NCSC guidance. It assesses:</p>
<ul class="list list-bullet">

View File

@@ -8,43 +8,20 @@
{% block maincolumn_content %}
<div class="usa-prose margin-bottom-10">
<h1 class="font-body-2xl margin-bottom-3">Contact us</h1>
<p>We are available at <a class="usa-link" href="mailto:notify-support@gsa.gov">notify-support@gsa.gov</a>.</p>
<p>Notify is designed to be easy to use.</p>
<ul class="list list-bullet">
<li>For information on personalization and data preparation, see <a href="/using-notify/guidance">Guidance</a>.</li>
<li>For help interpreting delivery reports, see <a href="/using-notify/delivery-status">Delivery Status</a>.</li>
<li>For details on pricing and what counts as a message part, see <a href="/using-notify/pricing"></a>Pricing.</li>
</ul>
<p>If you have other questions, we are available at <a class="usa-link" href="mailto:notify-support@gsa.gov">notify-support@gsa.gov</a>.</p>
<p>You can expect a response within one business day.</p>
<!-- <p>Notify.gov provides 24-hour online support.</p> -->
<!-- {% call form_wrapper() %}
{% if current_user.is_authenticated %}
{{ form.support_type }}
{% else %}
<p>
What do you need help with?
</p>
{{ form.who }}
{% endif %}
{{ page_footer('Continue') }}
{% endcall %}
<div class="grid-row">
<div class="grid-col-8">
<h2 class="font-body-lg">Office hours</h2>
<p>Our office hours are 9:30am to 5:30pm, Monday to Friday.</p>
<p>When you report a problem in office hours, well aim to read it within 30 minutes and reply within one working day.</p>
<h2 class="font-body-lg">Out-of-hours</h2>
<p>Outside office hours, response times depend on whether youre reporting an emergency.</p>
<p>If its an emergency, well reply within 30 minutes and update you every hour until the problems fixed.</p>
<p>If your problem is not an emergency, well reply within one working day.</p>
<p>A problem is only classed as an emergency if you have a live service and:</p>
<ul class="list list-bullet">
<li>nobody on your team can sign in</li>
<li>a technical difficulties error appears when you try to upload a file</li>
<li>a 500 response code appears when you try to send messages using the API</li>
</ul>
</div>
</div> -->
</div>
{% endblock %}

View File

@@ -7,37 +7,33 @@
{% block content_column_content %}
<h1 class="font-body-2xl margin-bottom-3">Trial mode</h1>
<p>When you add a new service it will start in trial mode. This lets you try out Notify.gov, with a few restrictions.</p>
<p>While your service is in trial mode you can only:</p>
<ul class="list list-bullet">
<li>send 50 text messages per day</li>
<li>send messages to yourself and other people in your team</li>
</ul>
{% if current_service and current_service.trial_mode %}
<p>
To remove these restrictions, you can <a class="usa-link" href="{{ url_for('.support') }}">request to go live</a>.</p>
{% else %}
<p>
To remove these restrictions:
</p>
<ol class="list list-number">
<li><a class="usa-link" href="{{ url_for('.sign_in') }}">Sign in to Notify</a>.</li>
<li>Go to the <b class="bold">Settings</b> page.</li>
<li>Select <b class="bold">Request to go live</b>.</li>
</ol>
{% endif %}
<p>
When we receive your request well get back to you within one working day.
</p>
<h2 class="font-body-lg" id="before-you-request-to-go-live">Before you request to go live</h2>
<p>You must:</p>
<h2 class="font-body-lg">Limits while in trial mode</h2>
<p>While your service is in trial mode you can only:</p>
<ul class="list list-bullet">
<li>add examples of the messages you want to send</li>
<li>update your settings so youre ready to send and receive messages</li>
<li>accept our terms of use</li>
<li>send 50 text message parts per day</li>
<li>send messages to yourself and other people in your team</li>
</ul>
<p>Each text message is made up of one or more parts.</p>
<ul class="list list-bullet">
<li>Generally, 160 characters is one part (and one text message).</li>
<li>A text message of 160-306 characters is two parts.</li>
</ul>
<p>For more information on how message parts are calculated, see
<a href="/using-notify/pricing">Pricing</a>.</p>
<h2 class="font-body-lg">Before going Live</h2>
<p>Before you request to make your service live so you can send messages to clients:</p>
<ul class="list list-bullet">
<li>Add examples of the messages you want to send into Notify.</li>
<li>Update your settings so youre ready to send and receive messages.</li>
<li>Review your Terms of Use in the Memorandum of Understanding.</li>
</ul>
<h3 class="font-body-lg">Going Live</h3>
<p>To remove the restrictions of Trial Mode and begin sending messages to clients complete the <a href="https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL">Live Campaign Form</a>.</p>
<p>We'll get back to you within one working day. </p>
{% endblock %}

View File

@@ -19,25 +19,25 @@
</div>
<div id='pill-selected-item'>
<div class='grid-row'>
<div class='grid-col-12'>
<h2 class="heading-small margin-bottom-1">Daily messages across all services</h2>
<p class="margin-0">You have sent {{ global_message_limit - daily_global_messages_remaining }} of your {{ global_message_limit }} daily messages allowance.</p>
<p class="margin-0"></p>You have {{ daily_global_messages_remaining }} messages remaining.</p>
<h2 class='heading-small margin-bottom-1'>Text messages</h2>
<h2 class='heading-small margin-bottom-1'>Text message parts</h2>
<div class="keyline-block">
You have sent
{{ big_number(sms_sent, 'messages of your', smaller=True) }}
{{ big_number(sms_free_allowance, 'free messages allowance.', smaller=True) }}
{{ big_number(sms_sent, 'text message parts of your', smaller=True) }}
{{ big_number(sms_free_allowance, 'free message parts allowance.', smaller=True) }}
<br />
You have
{% if sms_free_allowance > 0 %}
{{ big_number(sms_allowance_remaining, 'messages remaining.', smaller=True) }}
{{ big_number(sms_allowance_remaining, 'message parts remaining.', smaller=True) }}
{% endif %}
{# {% for row in sms_breakdown %}
{% if row.charged_units > 0 %}
{{ big_number(
row.charged_units,
row.charged_units,
'at {:.2f} pence per message'.format(row.rate * 100),
smaller=True
) }}
@@ -90,7 +90,7 @@
{{ item.month }}
{% endcall %}
{% for counts, template_type in [
(item.sms_counts.0, 'sms'),
(item.sms_counts.0, 'parts'),
] %}
{% call field(align='left') %}
{{ big_number(
@@ -126,7 +126,7 @@
{% endif %}
</ul>
{% endcall %}
{% endcall %}
</div>
{% endif %}
@@ -140,7 +140,7 @@
</div>
<div class="grid-col-8">
<p class="align-with-heading-copy">
What counts as 1 text message?<br />
What counts as 1 text message part?<br />
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a>.
</p>
</div>

View File

@@ -170,7 +170,7 @@ def convert_report_date_to_preferred_timezone(db_date_str_in_utc):
utc_date_obj = utc_date_obj.astimezone(pytz.utc)
preferred_timezone = pytz.timezone(get_user_preferred_timezone())
preferred_date_obj = utc_date_obj.astimezone(preferred_timezone)
preferred_tz_created_at = preferred_date_obj.strftime("%Y-%m-%d %H:%M:%S")
preferred_tz_created_at = preferred_date_obj.strftime("%Y-%m-%d %I:%M:%S %p")
return f"{preferred_tz_created_at} {get_user_preferred_timezone()}"

View File

@@ -0,0 +1 @@
from .templates import Environment # noqa

View File

@@ -0,0 +1,30 @@
from flask.templating import Environment as FlaskEnvironment
from jinja2 import select_autoescape
from app.utils.govuk_frontend_jinja.templates import Environment as NunjucksEnvironment
from app.utils.govuk_frontend_jinja.templates import (
NunjucksExtension,
NunjucksUndefined,
)
class Environment(NunjucksEnvironment, FlaskEnvironment):
pass
def init_govuk_frontend(app):
"""Use the govuk_frontend_jinja Jinja environment in a Flask app
>>> from flask import Flask
>>> app = Flask("cheeseshop_service")
>>> init_govuk_frontend(app)
"""
app.jinja_environment = Environment
app.select_jinja_autoescape = select_autoescape(
("html", "htm", "xml", "xhtml", "njk")
)
jinja_options = app.jinja_options.copy()
jinja_options["extensions"].append(NunjucksExtension)
jinja_options["undefined"] = NunjucksUndefined
app.jinja_options = jinja_options
return app

View File

@@ -0,0 +1,292 @@
import builtins
import os.path as path
import re
from collections.abc import Sized
import jinja2
import jinja2.ext
from jinja2.lexer import Token
from markupsafe import Markup
def njk_to_j2(template):
# Some component templates (such as radios) use `items` as the key of
# an object element. However `items` is also the name of a dictionary
# method in Python, and Jinja2 will prefer to return this attribute
# over the dict item. Handle specially.
template = re.sub(r"\.items\b", ".items__njk", template)
# Some component templates (such as radios) append the loop index to a
# string. As the loop index is an integer this causes a TypeError in
# Python. Jinja2 has an operator `~` for string concatenation that
# converts integers to strings.
template = template.replace("+ loop.index", "~ loop.index")
# The Character Count component in version 3 concatenates the word count
# with the hint text. As the word count is an integer this causes a
# TypeError in Python. Jinja2 has an operator `~` for string
# concatenation that converts integers to strings.
template = template.replace(
"+ (params.maxlength or params.maxwords) +",
"~ (params.maxlength or params.maxwords) ~",
)
# Nunjucks uses elseif, Jinja uses elif
template = template.replace("elseif", "elif")
# Some component templates (such as input) call macros with params as
# an object which has unqoted keys. This causes Jinja to silently
# ignore the values.
template = re.sub(
r"""^([ ]*)([^ '"#\r\n:]+?)\s*:""", r"\1'\2':", template, flags=re.M
)
# govukFieldset can accept a call block argument, however the Jinja
# compiler does not detect this as the macro body is included from
# the template file. A workaround is to patch the declaration of the
# macro to include an explicit caller argument.
template = template.replace(
"macro govukFieldset(params)", "macro govukFieldset(params, caller=none)"
)
# Many components feature an attributes field, which is supposed to be
# a dictionary. In the template for these components, the keys and values
# are iterated. In Python, the default iterator for a dict is .keys(), but
# we want .items().
# This only works because our undefined implements .items()
# We've tested this explicitly with: govukInput, govukCheckbox, govukTable,
# govukSummaryList
template = re.sub(
r"for attribute, value in (params|item|cell|action).attributes",
r"for attribute, value in \1.attributes.items()",
template,
flags=re.M,
)
# Some templates try to set a variable in an outer block, which is not
# supported in Jinja. We create a namespace in those templates to get
# around this.
template = re.sub(
r"""^([ ]*)({% set describedBy =( params.*describedBy if params.*describedBy else)? "" %})""",
r"\1{%- set nonlocal = namespace() -%}\n\1\2",
template,
flags=re.M,
)
# Change any references to describedBy to be nonlocal.describedBy,
# unless describedBy is a dictionary key (i.e. quoted or dotted).
template = re.sub(r"""(?<!['".])describedBy""", r"nonlocal.describedBy", template)
# govukSummaryList
template = re.sub(
r"""^([ ]*)({% set anyRowHasActions = false %})""",
r"\1{%- set nonlocal = namespace() -%}\n\1\2",
template,
flags=re.M,
)
template = re.sub(
r"""(?<!['".])anyRowHasActions""", r"nonlocal.anyRowHasActions", template
)
# govukRadios and govukCheckboxes
# Since both of these templates set describedBy before isConditional, we can use
# the existing nonlocal.
template = re.sub(
r"""(?<!['".])isConditional""", r"nonlocal.isConditional", template
)
# Issue#16: some component templates test the length of an array by trying
# to get an attribute `.length`. We need to handle this specially because
# .length isn't a thing in python
template = re.sub(r"\.length\b", ".length__njk", template)
# see `indent_njk`
template = re.sub(re.escape("| indent") + r"\b", "| indent_njk", template)
return template
def indent_njk(s, width=4, first=False, blank=False, indentfirst=None):
"""Return a copy of the string with each line indented by 4 spaces."""
# Copied from
# https://github.com/pallets/jinja/blob/a2f5e2c7972c4d5148c1c75c724e24950d8605bc/jinja2/filters.py#L536-L580
# to include an unreleased fix for https://github.com/pallets/jinja/pull/826
# which causes the file upload component to escape HTML markup.
# TODO: Remove once jinja2 2.11 is released and in use.
if indentfirst is not None:
first = indentfirst
indention = " " * width
newline = "\n"
if isinstance(s, Markup):
indention = Markup(indention)
newline = Markup(newline)
s += newline # this quirk is necessary for splitlines method
if blank:
rv = (newline + indention).join(s.splitlines())
else:
lines = s.splitlines()
rv = lines.pop(0)
if lines:
rv += newline + newline.join(
indention + line if line else line for line in lines
)
if first:
rv = indention + rv
return rv
class NunjucksExtension(jinja2.ext.Extension):
def filter_stream(self, stream):
if stream.filename and stream.filename.endswith(".njk"):
return self.filter_njk_stream(stream)
else:
return stream
def filter_njk_stream(self, stream):
for token in stream:
# patch strict equality operator `===`
if token.test("eq:==") and stream.current.test("assign:="):
yield Token(token.lineno, "name", "is")
yield Token(token.lineno, "name", "sameas")
stream.skip(1)
else:
yield token
def preprocess(self, source, name, filename=None):
if filename and filename.endswith(".njk"):
return njk_to_j2(source)
else:
return source
class NunjucksUndefined(jinja2.runtime.Undefined):
__slots__ = ()
# copied from https://github.com/pallets/jinja/commit/19133d40593ced72eb28e230588abcc70d8b9f82
def __getattr__(self, _):
"""Make undefined that is chainable, where both
__getattr__ and __getitem__ return itself rather than
raising an :exc:`UndefinedError`:
>>> foo = ChainableUndefined(name='foo')
>>> str(foo.bar['baz'])
''
>>> foo.bar['baz'] + 42
Traceback (most recent call last):
...
jinja2.exceptions.UndefinedError: 'foo' is undefined
"""
return self
__getitem__ = __getattr__
# Allow treating undefined as an (empty) dictionary.
# This works because Undefined is an iterable.
def items(self):
return self
# Allow escaping with Markup. This is required when
# autoescape is enabled. Debugging this issue was
# annoying; the error messages were not clear as to
# the cause of the issue (see upstream pull request
# for info https://github.com/pallets/jinja/pull/1047)
def __html__(self):
return str(self)
# attempt to behave a bit like js's `undefined` when concatenation is attempted
def __add__(self, other):
if isinstance(other, str):
return "undefined" + other
return super().__add__(other)
def __radd__(self, other):
if isinstance(other, str):
return other + "undefined"
return super().__radd__(other)
class NunjucksCodeGenerator(jinja2.compiler.CodeGenerator):
def visit_CondExpr(self, node, frame):
if not (self.filename or "").endswith(".njk"):
return super().visit_CondExpr(node, frame)
# else our replacement, which is based on that in
# https://github.com/pallets/jinja/blob/c4c4088945a2c12535f539be7f5453b9ca94666c/jinja2/compiler.py#L1613
def write_expr2():
if node.expr2 is not None:
return self.visit(node.expr2, frame)
# rather than complaining about a missing else
# clause we just assume it to be the empty
# string for nunjucks compatibility
return self.write('""')
self.write("(")
self.visit(node.expr1, frame)
self.write(" if ")
self.visit(node.test, frame)
self.write(" else ")
write_expr2()
self.write(")")
_njk_signature = "__njk"
_builtin_function_or_method_type = type({}.keys)
class Environment(jinja2.Environment):
code_generator_class = NunjucksCodeGenerator
def __init__(self, *args, **kwargs):
kwargs.setdefault("extensions", [NunjucksExtension])
kwargs.setdefault("undefined", NunjucksUndefined)
super().__init__(*args, **kwargs)
self.filters["indent_njk"] = indent_njk
def join_path(self, template, parent):
"""Enable the use of relative paths in template import statements"""
if template.startswith(("./", "../")):
return path.normpath(path.join(path.dirname(parent), template))
else:
return template
def _handle_njk(method_name):
def inner(self, obj, argument):
if isinstance(argument, str) and argument.endswith(_njk_signature):
# a njk-originated access will always be assuming a dict lookup before an attr
final_method_name = "getitem"
final_argument = argument[: -len(_njk_signature)]
else:
final_argument = argument
final_method_name = method_name
# pleasantly surprised that super() works in this context
retval = builtins.getattr(super(), final_method_name)(obj, final_argument)
if (
argument == f"length{_njk_signature}"
and isinstance(retval, jinja2.runtime.Undefined)
and isinstance(obj, Sized)
):
return len(obj)
if (
isinstance(argument, str)
and argument.endswith(_njk_signature)
and isinstance(retval, _builtin_function_or_method_type)
):
# the lookup has probably gone looking for attributes and found a builtin method. because
# any njk-originated lookup will have been made to prefer dict lookups over attributes, we
# can be fairly sure there isn't a dict key matching this - so we should just call this a
# failure.
return self.undefined(obj=obj, name=final_argument)
return retval
return inner
getitem = _handle_njk("getitem")
getattr = _handle_njk("getattr")

View File

@@ -1,6 +1,17 @@
# Notify Sprint Goals Log
## Sprint: A (1/3/24)
## Sprint: B (1/18/24)
2024 Sprints will be named after mythical creatures
| | Goals | Impact | Milestone(s) |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------|
| Engineering | Get `staging` stable and bugs fixed in order to do a prod push, finalize Login.gov implementation, complete epic to [remove phone numbers from db](https://github.com/GSA/notifications-api/issues/667), [calculate pre-send message parts](https://github.com/GSA/notifications-admin/issues/1027) to add to confirmation page, finish [E2E testing implementation](https://github.com/GSA/notifications-admin/issues/677), set up [opt-out lists by phone number](https://github.com/GSA/notifications-api/issues/312) | Install required auth, expand message send volume for partners, allow users to see how many parts a send will cost, improve app stability, partition opt-outs so that they aren't across the system | Building Notify.gov |
| UX | [Tackle `Usage and Performance` epic](https://github.com/GSA/notifications-admin/issues/1108) as highest priority group of user stories, break out UX work into more user-stories and correlating epics, prioritize epic/story order, improve [pre-send confirmation page](https://github.com/GSA/notifications-admin/issues/815)| Improve UX | Building Notify.gov|
| Security | Complete necessary control families | Aim to have package completed with enough time to allow for long assessment| Secure ATO |
| Content | Get [refreshed content for `Using Notify` pages](https://github.com/GSA/notifications-admin/issues/449) in place, make content [more clear about limits](https://github.com/GSA/notifications-admin/issues/963), make content clearer by [adding `parts` where necessary](https://github.com/GSA/notifications-admin/issues/963) | Improve clarity of message parts for users, bring existing content into current state| Building Notify.gov |
| Ops | Assist in Tech to Gov process, plan gap coverage for Tim's departure 2/9 |
## Sprint: Akhlut (1/3/24)
2024 Sprints will be named after mythical creatures
| | Goals | Impact | Milestone(s) |

223
poetry.lock generated
View File

@@ -41,24 +41,24 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
[[package]]
name = "bandit"
version = "1.7.6"
version = "1.7.7"
description = "Security oriented static analyser for python code."
optional = false
python-versions = ">=3.8"
files = [
{file = "bandit-1.7.6-py3-none-any.whl", hash = "sha256:36da17c67fc87579a5d20c323c8d0b1643a890a2b93f00b3d1229966624694ff"},
{file = "bandit-1.7.6.tar.gz", hash = "sha256:72ce7bc9741374d96fb2f1c9a8960829885f1243ffde743de70a19cee353e8f3"},
{file = "bandit-1.7.7-py3-none-any.whl", hash = "sha256:17e60786a7ea3c9ec84569fd5aee09936d116cb0cb43151023258340dbffb7ed"},
{file = "bandit-1.7.7.tar.gz", hash = "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a"},
]
[package.dependencies]
colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
GitPython = ">=3.1.30"
PyYAML = ">=5.3.1"
rich = "*"
stevedore = ">=1.20.0"
[package.extras]
test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0)"]
baseline = ["GitPython (>=3.1.30)"]
test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"]
toml = ["tomli (>=1.1.0)"]
yaml = ["PyYAML"]
@@ -529,47 +529,56 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
version = "41.0.7"
version = "42.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"},
{file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"},
{file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"},
{file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"},
{file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"},
{file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"},
{file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"},
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"},
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"},
{file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"},
{file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"},
{file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"},
{file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"},
{file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"},
{file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"},
]
[package.dependencies]
cffi = ">=1.12"
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["black", "check-sdist", "mypy", "ruff"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -915,37 +924,6 @@ files = [
{file = "geojson-3.1.0.tar.gz", hash = "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac"},
]
[[package]]
name = "gitdb"
version = "4.0.11"
description = "Git Object Database"
optional = false
python-versions = ">=3.7"
files = [
{file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"},
{file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"},
]
[package.dependencies]
smmap = ">=3.0.1,<6"
[[package]]
name = "gitpython"
version = "3.1.41"
description = "GitPython is a Python library used to interact with Git repositories"
optional = false
python-versions = ">=3.7"
files = [
{file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"},
{file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"},
]
[package.dependencies]
gitdb = ">=4.0.1,<5"
[package.extras]
test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"]
[[package]]
name = "govuk-bank-holidays"
version = "0.13"
@@ -960,28 +938,6 @@ files = [
[package.dependencies]
requests = "*"
[[package]]
name = "govuk-frontend-jinja"
version = "0.5.8-alpha"
description = "Tools to use the GOV.UK Design System with Jinja-powered Python apps"
optional = false
python-versions = "*"
files = []
develop = false
[package.dependencies]
jinja2 = "*"
[package.extras]
dev = ["pytest", "pytest-flakes", "pytest-helpers-namespace"]
flask = ["Flask"]
[package.source]
type = "git"
url = "https://github.com/alphagov/govuk-frontend-jinja.git"
reference = "v0.5.8-alpha"
resolved_reference = "15845e4cca3a05df72c6e13ec6a7e35acc682f52"
[[package]]
name = "greenlet"
version = "3.0.1"
@@ -1459,16 +1415,6 @@ files = [
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -1661,40 +1607,40 @@ files = [
[[package]]
name = "newrelic"
version = "9.5.0"
version = "9.6.0"
description = "New Relic Python Agent"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
{file = "newrelic-9.5.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:87c5ea1db9b2f573c7fb0961b7a856a91db0c9f130303de1839c542586b8dc87"},
{file = "newrelic-9.5.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8c62c07a69b86c54d3b9ce187551f36683ad8b70b6301273e7c1e89b121b8245"},
{file = "newrelic-9.5.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:847dd07961adec25c4c1128e8666f72ba0ab03640a44a65bcf25ffebd6fc2a75"},
{file = "newrelic-9.5.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:12be024a5dfd1667d05d7cce968189e2490b6f6d24f194695365607b2340be20"},
{file = "newrelic-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c381d300d904e2a5eb60953a29a729dbd4d8213f6dd629cecaa3033db70a9060"},
{file = "newrelic-9.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2e4ccd5fe97496806fe0ea1acb8b15791c1f012aa44fb047fc8016bcfb80ab9"},
{file = "newrelic-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:14ef5e94d490315a73cf4bea4cd0186b2ec68009e90ab2c16a0b501942fa8629"},
{file = "newrelic-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:306e54751fe1eba61884b0ed63e9583711b37e24db9c7b36725317c21aa609f5"},
{file = "newrelic-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ac2b3603cc781ef58e51a95b0b58220feb3e322c282c758c3bf5430398a47bc"},
{file = "newrelic-9.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5b0a3fbbd6ef1dca0d2434c8ddd15d945fef2cd343d09b3a352b8de825d2e83"},
{file = "newrelic-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab045fe37dcfd9c1fefc84f72373c54abd546d7517909fabd6920ea1d87c3f7f"},
{file = "newrelic-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e54492c5f70cf0951ffc97ddd71b3cf64d68b4d670012d7a7d5ff3dc19417dab"},
{file = "newrelic-9.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92968b7c6951f3ee0fe85a465ae79d448e587a3d10bf71d85b2cab09aa319776"},
{file = "newrelic-9.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847d75a08ca3ba356b6d6317618243e546dc7725251a453d4730ac6e9f043b71"},
{file = "newrelic-9.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec0fe958c5ed52cd8b496339df8661badbd24bdc290dea8744de94da705ccef0"},
{file = "newrelic-9.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a2af10be7893f60588e2103b2f5ff616baf8132ecce90d9a8c7bfb832da08748"},
{file = "newrelic-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2556ae2b9c13127dc46a6326c1ea682eae2c2b64b6873cfd4d5e177c7e0ce58a"},
{file = "newrelic-9.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e9818646a7a72332e5ab3853277187af5b0d51312c51585a5581453fbdc1508"},
{file = "newrelic-9.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b370179f644c0265e4467221e895dccc75440388c5cce8d6a89d150c6b265021"},
{file = "newrelic-9.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c514f82942f7759bc4a360d908c4be37b8764a3ff1589ba3ad8b4baf201bad36"},
{file = "newrelic-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15d4c2dd584c7604a03b2c73464ed3054cd8f0a513ed25832f1a3af4eb6e80ab"},
{file = "newrelic-9.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2af79b43e3244ce7775eb7a0bf2e8fe2cb1624f6eb1be03cb6c35f9e8a5be77"},
{file = "newrelic-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1dab3ef8baffebcf3f088a4131d93f3ac0e1fc08fa6045927c6bd2746b4decf4"},
{file = "newrelic-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78ca19f47b2bc38e18516c2a7c485eeac6979b271a1a5a4ac3b950581ca8a230"},
{file = "newrelic-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63314277c983cd412b87dfa3ed101b88c40ccab683a98046d76bf44a33cb7637"},
{file = "newrelic-9.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68917968adba4abde385be1c23d8fdb13714968883c3a3d1a83fb676ea614245"},
{file = "newrelic-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fb49e3a2e8ab9b509c8d276382f7ce0f6b4e51758b0edec38131ae583532b121"},
{file = "newrelic-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4638e6c28ebf2b5a8a860ae33105a1816425f6ece0113df626c88e5626b22f41"},
{file = "newrelic-9.5.0.tar.gz", hash = "sha256:2442449c4798989d92f527a82e60c446821c97896d739492b607bf6d4018ba1b"},
{file = "newrelic-9.6.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:09dad0db993402e166e37d99302c2ad5588b4ff1e5b814819540ca5ec2bd3cea"},
{file = "newrelic-9.6.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:aeadc0bd8140738af4d7d7ea9d0a3dfa7b6f37e02fe48c0a035ccb6cac881770"},
{file = "newrelic-9.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:16fb3bfde6dca0edab358c2b083386e2e764c463d85ffeb41cd6ce7a279bd7f6"},
{file = "newrelic-9.6.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1ba2fc6ce0b6b9a399a1e5db7e480ba51b431cb3cc6ba136a4c0e76877cf972d"},
{file = "newrelic-9.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe927f79db2dd5432a362aedde98543e93599b26d5c2e065bc563991be7f347"},
{file = "newrelic-9.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fefffff32e6605ddf26d51030ea1a41b9f7f606fccbb5f8d4b43c36d755acd1"},
{file = "newrelic-9.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0a68dd85a7cf4c5039ea94c8975f86a7c516dd492d8bf08976ab0a459918da9b"},
{file = "newrelic-9.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:74a9a49ef596aa9f7560f6bb1480480b8f9b652c7e03de173ef06b0dc2355799"},
{file = "newrelic-9.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26255aec6b29bc17e5e089bb18fbf004f4c765386ba61d1289681aa7ba538207"},
{file = "newrelic-9.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6f21f7a69294f64daa9be59c1c1e0eba889e85ed83afc67ed0a63c8efc9ff2b"},
{file = "newrelic-9.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5ecd44ff5b39138469778b5224a1a373867d21a0ca2389d093d99939cb422947"},
{file = "newrelic-9.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c6c4aa8b91dcb5403862c3e5efa57751a8b4e778576916bb22cafbfc03d47807"},
{file = "newrelic-9.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f02fb9166533f22fdb465f7a8d4b364178d94561b159cc9922f4d27491fd7d1"},
{file = "newrelic-9.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703ee92dda983eb4dcbcc300750a815ff3a5401995c3cdd587ac785b6d747e0e"},
{file = "newrelic-9.6.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ce4646554246c6577f5aea358516ad5cf2c35fcef0cd16dbd01bbe89b19b4ccb"},
{file = "newrelic-9.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5fae1f3addb422629c4d3e9d011de40629b4d078073ad0517ad42d4ddc7e8441"},
{file = "newrelic-9.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333bd5aedb4f814447ac3aa1a0cc76910a64f13385aa6481a97f87585af52cbf"},
{file = "newrelic-9.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a5aa22ce7813294ca5dc89a0d86ce4774bb9201028305975efdcf93ef36e0e"},
{file = "newrelic-9.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:01c0eb630bb18261241a37aa0a70cb6f706079a1f58f59f2bb64f26fda54ffc5"},
{file = "newrelic-9.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:eb82f31754dadd27676fe6f681b25db7af75eb854e332b92f1ea17584efc322c"},
{file = "newrelic-9.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38256e67b2d295229240c29e9949cdae40c9ac987d51f569f57f52ed2aef027f"},
{file = "newrelic-9.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f681c005b1d44ec01b3fe1a7b4ec589572f681f27130ea367155ac0245e133d4"},
{file = "newrelic-9.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:23d54298bc0a936a17e25fdca52c2eefbc50966985738e31d62cfe12ea3f60ba"},
{file = "newrelic-9.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f346aeb418c9da6cff4382cb4f50d314ae5126a6a6037f01224b0072eb30ff49"},
{file = "newrelic-9.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26560dba7cd36c5c787e3dfd6c3c5d1e8e8605c82dc7ffee143880456d8650e5"},
{file = "newrelic-9.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d168349041922f206b72fa30a930c0752d8b76b4a2fd7774d4cb2843aa804e32"},
{file = "newrelic-9.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:72b378b4aa4a52d2ac14be962f0de747f573b9912365981c0b3fc16bd12b5133"},
{file = "newrelic-9.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ab342ab7e3189e244e44238aa8563eeff3a5b5896b77d2d4dbf2c5d0bfe7d73"},
{file = "newrelic-9.6.0.tar.gz", hash = "sha256:3db642583cfee951f185696fe365b41d8aff733b23867e88de97de114e3cfcf8"},
]
[package.extras]
@@ -1731,7 +1677,7 @@ requests = ">=2.0.0"
[[package]]
name = "notifications-utils"
version = "0.2.6"
version = "0.2.8"
description = ""
optional = false
python-versions = ">=3.9,<3.12"
@@ -1749,7 +1695,7 @@ certifi = "^2023.7.22"
cffi = "^1.16.0"
charset-normalizer = "^3.1.0"
click = "^8.1.3"
cryptography = "^41.0.6"
cryptography = "^42.0.0"
flask = "^2.3.2"
flask-redis = "^0.4.0"
geojson = "^3.0.1"
@@ -1783,7 +1729,7 @@ werkzeug = "^3.0.1"
type = "git"
url = "https://github.com/GSA/notifications-utils.git"
reference = "HEAD"
resolved_reference = "f183d120a8b86e655405694adde1f0d95e4e5a51"
resolved_reference = "a48171e865eb83cf29c75751be7369f396cbe3e2"
[[package]]
name = "numpy"
@@ -2431,13 +2377,13 @@ six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "1.0.0"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
files = [
{file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
@@ -2876,17 +2822,6 @@ files = [
{file = "smartypants-2.0.1-py2.py3-none-any.whl", hash = "sha256:8db97f7cbdf08d15b158a86037cd9e116b4cf37703d24e0419a0d64ca5808f0d"},
]
[[package]]
name = "smmap"
version = "5.0.1"
description = "A pure Python implementation of a sliding window memory map manager"
optional = false
python-versions = ">=3.7"
files = [
{file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"},
{file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"},
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
@@ -3129,4 +3064,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.12"
content-hash = "4794f0a44c97a4e7b2b49341f56b826d10ac6652dfdbc29323f7e433e10dc65d"
content-hash = "be46f8e90d6213db5c54d62c3a364c0a2a7e50f5c8e5007ad662067a6191c51f"

View File

@@ -17,7 +17,6 @@ flask-login = "^0.6"
flask-talisman = "*"
flask-wtf = "^1.2"
govuk-bank-holidays = "==0.13"
govuk-frontend-jinja = {git = "https://github.com/alphagov/govuk-frontend-jinja.git", tag = "v0.5.8-alpha"}
gunicorn = {version = "==21.2.0", extras = ["eventlet"]}
humanize = "~=4.9"
itsdangerous = "~=2.1"
@@ -32,7 +31,7 @@ pyexcel-xls = "==0.7.0"
pyexcel-xlsx = "==0.6.0"
openpyxl = "==3.0.10"
pyproj = "==3.6.1"
python-dotenv = "==1.0.0"
python-dotenv = "==1.0.1"
pytz = "==2023.3.post1"
rtreelib = "==0.2.0"
werkzeug = "^3.0.1"

View File

@@ -88,6 +88,8 @@ The below steps rely on you first configuring access to the Terraform state in s
terraform plan
```
If the `terraform init` command fails, you may need to run `terraform init -upgrade` to make sure new module versions are picked up.
1. Apply changes with `terraform apply`.
1. Remove the space deployer service instance if it doesn't need to be used again, such as when manually running terraform once.

View File

@@ -3,7 +3,7 @@ terraform {
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.15.5"
version = "0.53.0"
}
}
}

View File

@@ -7,7 +7,7 @@ locals {
}
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
@@ -17,7 +17,7 @@ module "redis" {
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name

View File

@@ -3,7 +3,7 @@ terraform {
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.15.5"
version = "0.53.0"
}
}
@@ -12,7 +12,6 @@ terraform {
key = "admin.tfstate.demo"
encrypt = "true"
region = "us-gov-west-1"
profile = "notify-terraform-backend"
}
}

View File

@@ -11,7 +11,7 @@ data "cloudfoundry_space" "dev" {
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name

View File

@@ -3,7 +3,7 @@ terraform {
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.50.7"
version = "0.53.0"
}
}
}

View File

@@ -47,7 +47,7 @@ if [[ ! -s "secrets.auto.tfvars" ]]; then
fi
echo "Importing terraform state for $username"
terraform init
terraform init -upgrade
key_name=$username-admin-dev-key

View File

@@ -7,7 +7,7 @@ locals {
}
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
@@ -17,7 +17,7 @@ module "redis" {
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
@@ -50,7 +50,7 @@ module "api_network_route" {
# https://cloud.gov/docs/services/external-domain-service/#how-to-create-an-instance-of-this-service
###########################################################################
module "domain" {
source = "github.com/18f/terraform-cloudgov//domain?ref=v0.5.2"
source = "github.com/18f/terraform-cloudgov//domain?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name

View File

@@ -3,7 +3,7 @@ terraform {
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.15.5"
version = "0.53.0"
}
}
@@ -12,7 +12,6 @@ terraform {
key = "admin.tfstate.prod"
encrypt = "true"
region = "us-gov-west-1"
profile = "notify-terraform-backend"
}
}

View File

@@ -7,7 +7,7 @@ locals {
}
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
@@ -17,7 +17,7 @@ module "redis" {
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name

View File

@@ -3,7 +3,7 @@ terraform {
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.15.5"
version = "0.53.0"
}
}
@@ -12,7 +12,6 @@ terraform {
key = "admin.tfstate.sandbox"
encrypt = "true"
region = "us-gov-west-1"
profile = "notify-terraform-backend"
}
}

View File

@@ -3,7 +3,7 @@ terraform {
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "~> 0.15"
version = "0.53.0"
}
}
}

View File

@@ -7,7 +7,7 @@ locals {
}
module "redis" {
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//redis?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name
@@ -17,7 +17,7 @@ module "redis" {
}
module "logo_upload_bucket" {
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0"
source = "github.com/18f/terraform-cloudgov//s3?ref=v0.7.1"
cf_org_name = local.cf_org_name
cf_space_name = local.cf_space_name

View File

@@ -3,7 +3,7 @@ terraform {
required_providers {
cloudfoundry = {
source = "cloudfoundry-community/cloudfoundry"
version = "0.15.5"
version = "0.53.0"
}
}
@@ -12,7 +12,6 @@ terraform {
key = "admin.tfstate.stage"
encrypt = "true"
region = "us-gov-west-1"
profile = "notify-terraform-backend"
}
}

View File

@@ -71,49 +71,32 @@ FAKE_ONE_OFF_NOTIFICATION = {
"total": 1,
}
MOCK_ONE_OFF_JOB = {
"data": {
"api_key": "mocked_api_key",
"billable_units": 1,
"carrier": "mocked_carrier",
"client_reference": "mocked_client_reference",
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"email_address": "mocked_email@example.com",
"id": "mocked_user_id",
"name": "mocked_user",
},
"document_download_count": None,
"id": "mocked_notification_id",
"international": False,
"job": {"id": "mocked_job_id", "original_file_name": "mocked_file.txt"},
"job_row_number": 0,
"key_name": "mocked_key_name",
"key_type": "normal",
"normalised_to": "+12133166548",
"notification_type": "sms",
"personalisation": {"phonenumber": "+12133166548"},
"phone_prefix": "1",
"provider_response": "mocked_provider_response",
"rate_multiplier": 1.0,
"reference": "mocked_reference",
"reply_to_text": "mocked_reply_text",
"sent_at": "2024-01-04T20:43:53+00:00",
"sent_by": "mocked_sender",
"service": "mocked_service_id",
"status": "sending",
"template": {
"content": "((day of week)) and ((fave color))",
"id": "bd9caa7e-00ee-4c5a-839e-10ae1a7e6f73",
"name": "personalized",
"redact_personalisation": False,
"subject": None,
MOCK_JOBS = {
"data": [
{
"archived": False,
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"id": "mocked_user_id",
"name": "mocked_user",
},
"id": "mocked_notification_id",
"job_status": "finished",
"notification_count": 1,
"original_file_name": "mocked_file.csv",
"processing_finished": "2024-01-25T23:02:25+00:00",
"processing_started": "2024-01-25T23:02:24+00:00",
"scheduled_for": None,
"service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3",
"service_name": {"name": "Mock Texting Service"},
"statistics": [{"count": 1, "status": "sending"}],
"template": "6a456418-498c-4c86-b0cd-9403c14a216c",
"template_name": "Mock Template Name",
"template_type": "sms",
"version": 1,
},
"to": "+12133166548",
"updated_at": "2024-01-04T20:43:53+00:00",
}
"template_version": 3,
"updated_at": "2024-01-25T23:02:25+00:00",
}
]
}
@@ -309,7 +292,7 @@ def test_accepting_invite_removes_invite_from_session(
sample_invite["email_address"] = user["email_address"]
client_request.login(user)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -725,7 +708,7 @@ def test_new_invited_user_verifies_and_added_to_service(
token="thisisnotarealtoken",
_expected_redirect=url_for("main.register_from_invite"),
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,

View File

@@ -160,8 +160,8 @@ def test_can_show_notifications(
assert normalize_spaces(
first_row.select_one(".table-field-right-aligned .align-with-message-body").text
) in [
"Delivered 1 January at 02:01 US/Eastern",
"Delivered 1 January at 01:01 US/Eastern",
"Delivered 1 January at 02:00 US/Eastern",
"Delivered 1 January at 01:00 US/Eastern",
]
assert page_title in page.h1.text.strip()
@@ -661,31 +661,31 @@ def test_redacts_templates_that_should_be_redacted(
(
"email",
"temporary-failure",
"Inbox not accepting messages right now 27 September at 08:31 US/Eastern",
"Inbox not accepting messages right now 27 September at 08:30 US/Eastern",
False,
),
(
"email",
"permanent-failure",
"Email address does not exist 27 September at 08:31 US/Eastern",
"Email address does not exist 27 September at 08:30 US/Eastern",
False,
),
("email", "delivered", "Delivered 27 September at 08:31 US/Eastern", True),
("email", "delivered", "Delivered 27 September at 08:30 US/Eastern", True),
("sms", "created", "Sending since 27 September at 08:30 US/Eastern", True),
("sms", "sending", "Sending since 27 September at 08:30 US/Eastern", True),
(
"sms",
"temporary-failure",
"Phone not accepting messages right now 27 September at 08:31 US/Eastern",
"Phone not accepting messages right now 27 September at 08:30 US/Eastern",
False,
),
(
"sms",
"permanent-failure",
"Not delivered 27 September at 08:31 US/Eastern",
"Not delivered 27 September at 08:30 US/Eastern",
False,
),
("sms", "delivered", "Delivered 27 September at 08:31 US/Eastern", True),
("sms", "delivered", "Delivered 27 September at 08:30 US/Eastern", True),
],
)
def test_sending_status_hint_displays_correctly_on_notifications_page(

View File

@@ -85,49 +85,32 @@ FAKE_ONE_OFF_NOTIFICATION = {
"total": 1,
}
MOCK_ONE_OFF_JOB = {
"data": {
"api_key": "mocked_api_key",
"billable_units": 1,
"carrier": "mocked_carrier",
"client_reference": "mocked_client_reference",
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"email_address": "mocked_email@example.com",
"id": "mocked_user_id",
"name": "mocked_user",
},
"document_download_count": None,
"id": "mocked_notification_id",
"international": False,
"job": {"id": "mocked_job_id", "original_file_name": "mocked_file.txt"},
"job_row_number": 0,
"key_name": "mocked_key_name",
"key_type": "normal",
"normalised_to": "+12133166548",
"notification_type": "sms",
"personalisation": {"phonenumber": "+12133166548"},
"phone_prefix": "1",
"provider_response": "mocked_provider_response",
"rate_multiplier": 1.0,
"reference": "mocked_reference",
"reply_to_text": "mocked_reply_text",
"sent_at": "2024-01-04T20:43:53+00:00",
"sent_by": "mocked_sender",
"service": "mocked_service_id",
"status": "sending",
"template": {
"content": "((day of week)) and ((fave color))",
"id": "bd9caa7e-00ee-4c5a-839e-10ae1a7e6f73",
"name": "personalized",
"redact_personalisation": False,
"subject": None,
MOCK_JOBS = {
"data": [
{
"archived": False,
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"id": "mocked_user_id",
"name": "mocked_user",
},
"id": "mocked_notification_id",
"job_status": "finished",
"notification_count": 1,
"original_file_name": "mocked_file.csv",
"processing_finished": "2024-01-25T23:02:25+00:00",
"processing_started": "2024-01-25T23:02:24+00:00",
"scheduled_for": None,
"service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3",
"service_name": {"name": "Mock Texting Service"},
"statistics": [{"count": 1, "status": "sending"}],
"template": "6a456418-498c-4c86-b0cd-9403c14a216c",
"template_name": "Mock Template Name",
"template_type": "sms",
"version": 1,
},
"to": "+12133166548",
"updated_at": "2024-01-04T20:43:53+00:00",
}
"template_version": 3,
"updated_at": "2024-01-25T23:02:25+00:00",
}
]
}
stub_template_stats = [
@@ -218,7 +201,7 @@ def test_get_started(
"app.template_statistics_client.get_template_statistics_for_service",
return_value=copy.deepcopy(stub_template_stats),
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -248,7 +231,7 @@ def test_get_started_is_hidden_once_templates_exist(
"app.template_statistics_client.get_template_statistics_for_service",
return_value=copy.deepcopy(stub_template_stats),
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -275,7 +258,7 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
mock_get_inbound_sms_summary,
):
service_one["permissions"] = []
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -303,7 +286,6 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
mock_get_inbound_sms_summary,
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -312,7 +294,8 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
banner = page.select("a.banner-dashboard")[1]
assert (
normalize_spaces(banner.text)
@@ -335,7 +318,6 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
mock_get_inbound_sms_summary_with_no_messages,
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -344,7 +326,8 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
banner = page.select("a.banner-dashboard")[1]
assert normalize_spaces(banner.text) == "0 text messages received"
assert banner["href"] == url_for("main.inbox", service_id=SERVICE_ONE_ID)
@@ -585,7 +568,7 @@ def test_should_show_recent_templates_on_dashboard(
"app.template_statistics_client.get_template_statistics_for_service",
return_value=copy.deepcopy(stub_template_stats),
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -642,7 +625,7 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
"app.template_statistics_client.get_template_statistics_for_service",
return_value=stats,
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -782,7 +765,6 @@ def test_should_show_upcoming_jobs_on_dashboard(
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -792,7 +774,8 @@ def test_should_show_upcoming_jobs_on_dashboard(
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mock_get_scheduled_job_stats.assert_called_once_with(SERVICE_ONE_ID)
assert normalize_spaces(page.select_one("main h2").text) == ("In the next few days")
@@ -824,7 +807,7 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_count_is_0(
"soonest_scheduled_for": None,
},
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -850,7 +833,7 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_service_has_no_jobs(
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -904,7 +887,7 @@ def test_correct_font_size_for_big_numbers(
service_one["permissions"] = permissions
mocker.patch("app.main.views.dashboard.get_dashboard_totals", return_value=totals)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -938,7 +921,6 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -947,7 +929,8 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
"main.service_dashboard",
service_id=SERVICE_ONE_ID,
)
mock_get_jobs.assert_called_once_with(SERVICE_ONE_ID)
mock_get_jobs.assert_called_with(SERVICE_ONE_ID)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
for filename in {
"export 1/1/2016.xls",
"all email addresses.xlsx",
@@ -992,8 +975,8 @@ def test_usage_page(
sms_column = normalize_spaces(annual_usage[0].text)
assert (
"You have sent 251,800 messages of your 250,000 free messages allowance. You have 0 messages remaining."
in sms_column
"You have sent 251,800 text message parts of your 250,000 free message parts allowance."
" You have 0 message parts remaining." in sms_column
)
assert "$29.85 spent" not in sms_column
assert "1,500 at 1.65 pence" not in sms_column
@@ -1029,8 +1012,8 @@ def test_usage_page_no_sms_spend(
annual_usage = page.find_all("div", {"class": "keyline-block"})
sms_column = normalize_spaces(annual_usage[0].text)
assert (
"You have sent 1,000 messages of your 250,000 free messages allowance. You have 249,000 messages remaining."
in sms_column
"You have sent 1,000 text message parts of your 250,000 free message parts allowance."
" You have 249,000 message parts remaining." in sms_column
)
assert "$0.00 spent" not in sms_column
assert "pence per message" not in sms_column
@@ -1106,7 +1089,7 @@ def test_usage_page_with_0_free_allowance(
sms_column = normalize_spaces(annual_usage[0].text)
assert (
"You have sent 251,800 messages of your 0 free messages allowance. You have"
"You have sent 251,800 text message parts of your 0 free message parts allowance. You have"
in sms_column
)
assert "free allowance remaining" not in sms_column
@@ -1209,7 +1192,7 @@ def test_menu_send_messages(
):
service_one["permissions"] = ["email", "sms"]
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1246,7 +1229,7 @@ def test_menu_manage_service(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1283,7 +1266,7 @@ def test_menu_main_settings(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1319,7 +1302,7 @@ def test_menu_manage_api_keys(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1359,7 +1342,7 @@ def test_menu_all_services_for_platform_admin_user(
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1404,7 +1387,7 @@ def test_route_for_service_permissions(
mocker.patch(
"app.service_api_client.get_global_notification_count", side_effect=_get
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1460,7 +1443,7 @@ def test_service_dashboard_updates_gets_dashboard_totals(
"sms": {"requested": 456, "delivered": 0, "failed": 0},
},
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1586,7 +1569,7 @@ def test_org_breadcrumbs_do_not_show_if_service_has_no_org(
mock_get_annual_usage_for_service,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1653,7 +1636,7 @@ def test_org_breadcrumbs_show_if_user_is_a_member_of_the_services_org(
id_=ORGANISATION_ID,
),
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1689,7 +1672,7 @@ def test_org_breadcrumbs_do_not_show_if_user_is_a_member_of_the_services_org_but
)
mocker.patch("app.models.service.Organization")
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1727,7 +1710,7 @@ def test_org_breadcrumbs_show_if_user_is_platform_admin(
),
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1763,7 +1746,7 @@ def test_breadcrumb_shows_if_service_is_suspended(
"app.service_api_client.get_service", return_value={"data": service_one_json}
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1797,7 +1780,7 @@ def test_service_dashboard_shows_usage(
"count": 500,
},
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,
@@ -1840,7 +1823,7 @@ def test_service_dashboard_shows_free_allowance(
}
],
)
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
@@ -1864,7 +1847,7 @@ def test_service_dashboard_shows_batched_jobs(
mock_get_annual_usage_for_service,
mock_get_free_sms_fragment_limit,
):
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",
return_value=FAKE_ONE_OFF_NOTIFICATION,

View File

@@ -90,7 +90,7 @@ def test_should_show_page_for_one_job(
assert page.h1.text.strip() == "thisisatest.csv"
assert " ".join(page.find("tbody").find("tr").text.split()) == (
"2021234567 template content Delivered 1 January at 06:10 US/Eastern"
"2021234567 template content Delivered 1 January at 06:09 US/Eastern"
)
assert page.find("div", {"data-key": "notifications"})["data-resource"] == url_for(
"main.view_job_updates",
@@ -109,7 +109,7 @@ def test_should_show_page_for_one_job(
assert page.find("span", {"id": "time-left"}).text == "Data available for 7 days"
assert normalize_spaces(page.select_one("tbody tr").text) == normalize_spaces(
"2021234567 " "template content " "Delivered 1 January at 06:10 US/Eastern"
"2021234567 " "template content " "Delivered 1 January at 06:09 US/Eastern"
)
assert page.select_one("tbody tr a")["href"] == url_for(
"main.view_notification",
@@ -349,9 +349,10 @@ def test_should_show_scheduled_job(
job_id=fake_uuid,
)
assert normalize_spaces(page.select("main p")[1].text) == (
assert normalize_spaces(page.select("main div p")[1].text) == (
"Sending Two week reminder today at 00:00 US/Eastern"
)
assert page.select("main p a")[0]["href"] == url_for(
"main.view_template_version",
service_id=SERVICE_ONE_ID,
@@ -423,7 +424,7 @@ def test_should_show_updates_for_one_job_as_json(
assert "2021234567" in content["notifications"]
assert "Status" in content["notifications"]
assert "Delivered" in content["notifications"]
assert "00:01" in content["notifications"]
assert "00:00" in content["notifications"]
assert "Sent by Test User on 1 January at 00:00" in content["status"]
@@ -465,7 +466,7 @@ def test_should_show_updates_for_scheduled_job_as_json(
assert "2021234567" in content["notifications"]
assert "Status" in content["notifications"]
assert "Delivered" in content["notifications"]
assert "00:01" in content["notifications"]
assert "00:00" in content["notifications"]
assert "Sent by Test User on 1 June at 16:00" in content["status"]
@@ -481,3 +482,22 @@ def test_should_show_updates_for_scheduled_job_as_json(
@freeze_time("2016-01-10 12:00:00.000000")
def test_time_left(job_created_at, expected_message):
assert get_time_left(job_created_at) == expected_message
def test_should_show_message_note(
client_request,
mock_get_service_template,
mock_get_scheduled_job,
mock_get_service_data_retention,
mock_get_notifications,
fake_uuid,
):
page = client_request.get(
"main.view_job",
service_id=SERVICE_ONE_ID,
job_id=fake_uuid,
)
assert normalize_spaces(page.select_one("main p.notification-status").text) == (
"Messages will remain in pending state until carrier status is received, typically 5 minutes."
)

View File

@@ -57,49 +57,32 @@ FAKE_ONE_OFF_NOTIFICATION = {
"total": 1,
}
MOCK_ONE_OFF_JOB = {
"data": {
"api_key": "mocked_api_key",
"billable_units": 1,
"carrier": "mocked_carrier",
"client_reference": "mocked_client_reference",
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"email_address": "mocked_email@example.com",
"id": "mocked_user_id",
"name": "mocked_user",
},
"document_download_count": None,
"id": "mocked_notification_id",
"international": False,
"job": {"id": "mocked_job_id", "original_file_name": "mocked_file.txt"},
"job_row_number": 0,
"key_name": "mocked_key_name",
"key_type": "normal",
"normalised_to": "+12133166548",
"notification_type": "sms",
"personalisation": {"phonenumber": "+12133166548"},
"phone_prefix": "1",
"provider_response": "mocked_provider_response",
"rate_multiplier": 1.0,
"reference": "mocked_reference",
"reply_to_text": "mocked_reply_text",
"sent_at": "2024-01-04T20:43:53+00:00",
"sent_by": "mocked_sender",
"service": "mocked_service_id",
"status": "sending",
"template": {
"content": "((day of week)) and ((fave color))",
"id": "bd9caa7e-00ee-4c5a-839e-10ae1a7e6f73",
"name": "personalized",
"redact_personalisation": False,
"subject": None,
MOCK_JOBS = {
"data": [
{
"archived": False,
"created_at": "2024-01-04T20:43:52+00:00",
"created_by": {
"id": "mocked_user_id",
"name": "mocked_user",
},
"id": "mocked_notification_id",
"job_status": "finished",
"notification_count": 1,
"original_file_name": "mocked_file.csv",
"processing_finished": "2024-01-25T23:02:25+00:00",
"processing_started": "2024-01-25T23:02:24+00:00",
"scheduled_for": None,
"service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3",
"service_name": {"name": "Mock Texting Service"},
"statistics": [{"count": 1, "status": "sending"}],
"template": "6a456418-498c-4c86-b0cd-9403c14a216c",
"template_name": "Mock Template Name",
"template_type": "sms",
"version": 1,
},
"to": "+12133166548",
"updated_at": "2024-01-04T20:43:53+00:00",
}
"template_version": 3,
"updated_at": "2024-01-25T23:02:25+00:00",
}
]
}
@@ -139,7 +122,7 @@ def test_sign_out_user(
with client_request.session_transaction() as session:
assert session.get("user_id") is not None
# Check we are logged in
mocker.patch("app.job_api_client.get_job", return_value=MOCK_ONE_OFF_JOB)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.notification_api_client.get_notifications_for_service",

View File

@@ -1955,7 +1955,7 @@ def test_set_template_sender(
"sms",
False,
"a" * 160,
"Will be charged as 2 text messages",
"Will be charged as 1 text message",
None,
),
(
@@ -1969,7 +1969,7 @@ def test_set_template_sender(
"sms",
True,
"a" * 147,
"Will be charged as 2 text messages",
"Will be charged as 1 text message",
None,
),
(
@@ -1984,7 +1984,7 @@ def test_set_template_sender(
"sms",
False,
"a" * 918,
"Will be charged as 7 text messages",
"Will be charged as 6 text messages",
None,
),
(
@@ -2025,11 +2025,8 @@ def test_set_template_sender(
(
"sms",
False,
# The length of this string in bytes is 210, needing two fragments.
# Seems like previous calculation was wrong unless the number of characters
# somehow has priority over the number of bytes in a fragment (?)
"" * 70,
"Will be charged as 2 text messages",
"Will be charged as 1 text message. Use of characters outside the IEC_8859-1 character set may increase the message fragment count, resulting in additional charges, and these IEC_8859-1 characters may not display properly on some older mobile devices.", # noqa E501
None,
),
(
@@ -2043,11 +2040,7 @@ def test_set_template_sender(
"sms",
False,
"" * 918,
# The length of this string in bytes is 2754. Divide by 140 and we get 19. Then round up.
# Don't know why it was previously calculated as 14. They seem to have charged by characters
# rather than length of the fragment in bytes.
# "Will be charged as 14 text messages",
"Will be charged as 20 text messages",
"Will be charged as 14 text messages",
None,
),
(

View File

@@ -91,14 +91,14 @@ def get_notifications_csv_mock(
None,
[
"Recipient,Template,Type,Sent by,Job,Carrier,Carrier Response,Status,Time\n",
"foo@bar.com,foo,sms,,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 US/Eastern\r\n",
"foo@bar.com,foo,sms,,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n",
],
),
(
"Anne Example",
[
"Recipient,Template,Type,Sent by,Job,Carrier,Carrier Response,Status,Time\n",
"foo@bar.com,foo,sms,Anne Example,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 US/Eastern\r\n", # noqa
"foo@bar.com,foo,sms,Anne Example,,ATT Mobility,Did not like it,Delivered,1943-04-19 08:00:00 AM US/Eastern\r\n", # noqa
],
),
],
@@ -151,7 +151,7 @@ def test_generate_notifications_csv_without_job(
"ATT Mobility",
"Did not like it",
"Delivered",
"1943-04-19 08:00:00 US/Eastern",
"1943-04-19 08:00:00 AM US/Eastern",
],
),
(
@@ -187,7 +187,7 @@ def test_generate_notifications_csv_without_job(
"ATT Mobility",
"Did not like it",
"Delivered",
"1943-04-19 08:00:00 US/Eastern",
"1943-04-19 08:00:00 AM US/Eastern",
],
),
(
@@ -223,7 +223,7 @@ def test_generate_notifications_csv_without_job(
"ATT Mobility",
"Did not like it",
"Delivered",
"1943-04-19 08:00:00 US/Eastern",
"1943-04-19 08:00:00 AM US/Eastern",
],
),
],
@@ -398,4 +398,4 @@ def test_get_errors_for_csv(
def test_convert_report_date_to_preferred_timezone():
original = "2023-11-16 05:00:00"
altered = convert_report_date_to_preferred_timezone(original)
assert altered == "2023-11-16 00:00:00 US/Eastern"
assert altered == "2023-11-16 12:00:00 AM US/Eastern"