mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-22 23:36:26 -04:00
Compare commits
83 Commits
fixing-the
...
02-08-2024
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad34ca0a11 | ||
|
|
df54b066f9 | ||
|
|
338a6426c3 | ||
|
|
f37903ff6d | ||
|
|
802ebfa243 | ||
|
|
e18054d2d8 | ||
|
|
21c11adb5e | ||
|
|
266b530e3a | ||
|
|
6af738e0de | ||
|
|
27385bc45a | ||
|
|
f097b9ca07 | ||
|
|
50e3560c6f | ||
|
|
761c7bd0fd | ||
|
|
4da5c14751 | ||
|
|
0f167a91cc | ||
|
|
7c5674c4e1 | ||
|
|
6d3c92de28 | ||
|
|
e40a493f9b | ||
|
|
4e488fa079 | ||
|
|
5d480b3f53 | ||
|
|
070955175d | ||
|
|
a5eb311f83 | ||
|
|
6b4a5d009c | ||
|
|
1f12dd3908 | ||
|
|
0e64ff1c20 | ||
|
|
f7924320df | ||
|
|
1b4754386c | ||
|
|
a27519ff68 | ||
|
|
b72430af63 | ||
|
|
35b38f838c | ||
|
|
45ffa70c3c | ||
|
|
bd33fe4750 | ||
|
|
768f1986e8 | ||
|
|
e48cb94ccb | ||
|
|
0fc14131b1 | ||
|
|
6e05fd5147 | ||
|
|
c79c824b24 | ||
|
|
524a1e4687 | ||
|
|
9513069b42 | ||
|
|
af8309a2ce | ||
|
|
37086cb03b | ||
|
|
3ac9d44b2a | ||
|
|
d5210427f0 | ||
|
|
da1e2a2700 | ||
|
|
d9875ef287 | ||
|
|
cdd262365f | ||
|
|
195c68037b | ||
|
|
d5daab67cf | ||
|
|
782ec1c5eb | ||
|
|
fd8981d5cd | ||
|
|
1709d3279b | ||
|
|
2126ba8038 | ||
|
|
c9ed1ab59d | ||
|
|
a38518777c | ||
|
|
3379259e14 | ||
|
|
ede82d76d6 | ||
|
|
5970dd8055 | ||
|
|
65bccb3f95 | ||
|
|
75a4e1ae6d | ||
|
|
8baf9682a1 | ||
|
|
6b00cf1270 | ||
|
|
490174ab71 | ||
|
|
f15643c04d | ||
|
|
d253b601a2 | ||
|
|
9b0dc3dc3c | ||
|
|
443c7d9992 | ||
|
|
33433a3822 | ||
|
|
c7a393e620 | ||
|
|
ba3a229d60 | ||
|
|
17b1352f01 | ||
|
|
a1440ae36f | ||
|
|
d49fb8867e | ||
|
|
98b11dc3c4 | ||
|
|
6fe29a2bdd | ||
|
|
b463d420ef | ||
|
|
2a9f60c32e | ||
|
|
adda14342b | ||
|
|
d3c3183573 | ||
|
|
25305d0706 | ||
|
|
0e110201d6 | ||
|
|
11a85d688a | ||
|
|
82863b02ce | ||
|
|
27c74ca773 |
22
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
22
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
@@ -5,8 +5,6 @@ title: "Bug: [Brief description of the bug]"
|
||||
labels:
|
||||
- "Type: Bug"
|
||||
|
||||
assignees:
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -29,6 +27,17 @@ body:
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: In which environment did you experience the bug?
|
||||
options:
|
||||
- staging
|
||||
- demo
|
||||
- production
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: page
|
||||
attributes:
|
||||
@@ -36,6 +45,13 @@ body:
|
||||
description: please include URLs of where you saw the bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproSteps
|
||||
attributes:
|
||||
label: "Detail the steps for someone to reproduce"
|
||||
description: "Reproduction steps"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
@@ -52,4 +68,4 @@ body:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
required: false
|
||||
|
||||
|
||||
82
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: "QA Checklist"
|
||||
description: "Use this template to manually QA for prod push"
|
||||
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: Logging_in
|
||||
attributes:
|
||||
label: Log In
|
||||
options:
|
||||
- label: notify-staging.app.cloud.gov resolves
|
||||
- label: Clicking sign in takes you to login page
|
||||
- label: 2FA code is sent promptly
|
||||
- label: 2FA code input screen appears as expected
|
||||
- label: After code is entered, you are brought to the “choose service” screen
|
||||
- label: Choosing a service brings you to the dashboard for that service
|
||||
|
||||
- type: checkboxes
|
||||
id: send_template
|
||||
attributes:
|
||||
label: Create a template and send
|
||||
options:
|
||||
- label: Clicking “Send messages” brings you to templates page
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Uploading a csv is quick and automatically directs you to the preview/send page
|
||||
- label: Formatting looks clean and in place
|
||||
- label: Message dashboard populates and shows “pending” until call is made to AWS for delivery receipt
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__copied_template
|
||||
attributes:
|
||||
label: Send a message from copied template
|
||||
options:
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Clicking “copy an existing template” allows you to choose from pre-existing template
|
||||
- label: After editing, the new template saves and you prepare to send
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__existing_template
|
||||
attributes:
|
||||
label: Send a message from existing template
|
||||
options:
|
||||
- label: Clicking on existing template brings you to preview screen with `prepare to send` button
|
||||
- label: All else above persists as true (formatting, send, dashboard, etc)
|
||||
|
||||
- type: checkboxes
|
||||
id: download_report
|
||||
attributes:
|
||||
label: Download the csv delivery report
|
||||
options:
|
||||
- label: Clicking on the csv download directly after a message send downloads a report that contains the data we expect
|
||||
- label: The batch report appears on the main dashboard batch table with accurate information
|
||||
- label: Clicking on the csv download link from the main dashboard downloads a report that contains the data we expect
|
||||
|
||||
- type: checkboxes
|
||||
id: invite_member
|
||||
attributes:
|
||||
label: Invite a team member
|
||||
options:
|
||||
- label: Clicking on “Team” in settings brings you to team member screen
|
||||
- label: Clicking “Invite a team member” brings you to email and permissions screen
|
||||
- label: Clicking invite team member sends an invite and shows confirmation
|
||||
- label: Invite email provides live link that works to register new team member
|
||||
|
||||
- type: checkboxes
|
||||
id: delete_member
|
||||
attributes:
|
||||
label: Delete a team member
|
||||
options:
|
||||
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that person’s profile
|
||||
- label: Clicking on “Remove this team member” prompts a confirmation
|
||||
- label: Clicking the confirmation removes the team member
|
||||
|
||||
- type: checkboxes
|
||||
id: sign_out
|
||||
attributes:
|
||||
label: Sign Out
|
||||
options:
|
||||
- label: Hitting “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
14
.github/workflows/deploy-demo.yml
vendored
14
.github/workflows/deploy-demo.yml
vendored
@@ -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
|
||||
|
||||
15
.github/workflows/deploy-prod.yml
vendored
15
.github/workflows/deploy-prod.yml
vendored
@@ -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
|
||||
|
||||
3
.profile
3
.profile
@@ -4,3 +4,6 @@
|
||||
##
|
||||
|
||||
export NEW_RELIC_PROXY_HOST=$egress_proxy
|
||||
export http_proxy=$egress_proxy
|
||||
export https_proxy=$egress_proxy
|
||||
export no_proxy="apps.internal"
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
app/assets/images/product/security-review-message.png
Normal file
BIN
app/assets/images/product/security-review-message.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -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;
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
# Get started
|
||||
|
||||
|
||||
1. ## Check if Notify.gov is right for you
|
||||
<p>Read about our <a class="usa-link" href="{{ url_for('main.features') }}">features</a>, <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a> and <a class="usa-link" href="{{ url_for('main.roadmap') }}">roadmap</a>.</p>
|
||||
1. ## Everyone starts in [Trial Mode](/using-notify/trial-mode)
|
||||
Explore Notify, add team members, and practice [sending messages to teammates](/using-notify/trial-mode).
|
||||
|
||||
2. ## Personalize content
|
||||
Learn how to [personalize messages](/using-notify/guidance) to increase response.
|
||||
|
||||
2. ## Create an account
|
||||
{% if not current_user.is_authenticated %}
|
||||
<p><a class="usa-link" href="{{ url_for('.register') }}">Create an account</a> for free and add your first Notify service. When you add a new service it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% else %}
|
||||
<p>Create an account for free and add your first Notify service. When you add a new service, it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% endif %}
|
||||
|
||||
3. ## Write some messages
|
||||
{% if True %}
|
||||
<p>Add message templates with examples of the content you plan to send. You can use our <a class="usa-link" href="{{ url_for('main.guidance_index') }}">guidance</a> to help you.</p>
|
||||
{% endif %}
|
||||
|
||||
4. ## Set up your service
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>Review your settings to add message branding and sender information.</p>
|
||||
<p>Add team members and check their permissions.</p>
|
||||
{% else %}
|
||||
<p>Review your <a class="usa-link" href="{{ url_for('.service_settings', service_id=current_service.id) }}">settings</a> to add message branding and sender information.</p>
|
||||
<p>Add <a class="usa-link" href="{{ url_for('.manage_users', service_id=current_service.id) }}">team members</a> and check their permissions.</p>
|
||||
{% endif %}
|
||||
3. ## Check delivery status
|
||||
[Analyze the delivery](/using-notify/delivery-status) of your messages and download reports
|
||||
|
||||
4. ## View messaging limits
|
||||
Learn about message _parts_ and [how limits are calculated](/using-notify/pricing).
|
||||
|
||||
5. ## Start sending messages
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>When you’re ready to send messages to people outside your team, go to the <b class="bold">Settings</b> page and select <b class="bold">Request to go live</b>. We’ll approve your request within one working day.</p>
|
||||
{% else %}
|
||||
<p>You should <a class="usa-link" href="{{ url_for('.support') }}">request to go live</a> when you’re ready to send messages to people outside your team. We’ll approve your request within one working day.</p>
|
||||
{% endif %}
|
||||
To remove the restrictions of Trial Mode and begin sending messages to clients complete the [Live Campaign Form](https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL).
|
||||
We’ll respond within one business day.
|
||||
|
||||
<!-- <li class="get-started-list__item">
|
||||
<h2 class="usa-process-list__heading line-height-sans-1">Set up an API integration (optional)</h2>
|
||||
<p>You can use the Notify API to send messages automatically.</p>
|
||||
<p>Our <a class="usa-link" href="{{ url_for('main.documentation') }}">documentation</a> explains how to integrate the API with a web application or back office system.</p>
|
||||
</li> -->
|
||||
|
||||
<!-- <p>Check <a class="usa-link" href="{{ url_for('main.how_to_pay') }}">how to pay</a> if you’re planning to exceed the <a class="usa-link" href="{{ url_for('.pricing', _anchor='text-messages') }}">free text message allowance</a>.</p> -->
|
||||
### Questions?
|
||||
[Contact the Notify team](/support)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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  (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>
|
||||
|
||||
@@ -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”. You’ll see existing templates.</li>
|
||||
<li>Add a new template or choose an existing template and select Edit.</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="font-body-lg" id="links">Add links</h3>
|
||||
|
||||
<p>When composing a text message, write URLs in full and Notify will convert them into links for you.
|
||||
</p>
|
||||
|
||||
<p>You cannot convert text into a link.
|
||||
<!-- Research shows that people like to check a URL looks genuine before clicking the link in an email. This is hard to do if the URL is hidden behind clickable link text.-->
|
||||
</p>
|
||||
|
||||
<p>We do not recommend using a third-party link shortening service because:</p>
|
||||
{# Add links #}
|
||||
<h2 class="padding-top-1" id="add-links">Add links</h2>
|
||||
<p>When composing a text message, links to websites or online applications can help your recipient respond quickly.</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>your users cannot see where the link will take them</li>
|
||||
<li>your link might stop working if there’s a service outage</li>
|
||||
<li>you can no longer control where the redirect goes</li>
|
||||
<li>Write URLs in full and Notify will convert them into links for you. Note that you cannot hyperlink text in Notify
|
||||
messages.</li>
|
||||
<li>For link click tracking, you can consider adding campaign parameters to URLs.</li>
|
||||
<li>All links should point to a government domain.</li>
|
||||
<li>Link directly to where your recipient needs to take action, not to more information.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="font-body-lg" id="personalised-content">Personalize your content</h3>
|
||||
<h3>About link-shortening services</h3>
|
||||
<p>We do not recommend using a third-party link-shortening service because:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>Your recipient cannot see where the link will take them, which could make them suspect the link is spam/scam.</li>
|
||||
<li>Your link might stop working if there’s a service outage.</li>
|
||||
<li>You can no longer control where the redirect goes.</li>
|
||||
</ul>
|
||||
|
||||
<p>To personalize the content of your messages, add a placeholder to the template.</p>
|
||||
{# Personalize content #}
|
||||
<h2 class="padding-top-1" id="personalize-content">Personalize your content</h2>
|
||||
<p>Personalizing your content can increase response rates and help the recipient know the text is legitimate.</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>Including a person’s first name increases response rates.</li>
|
||||
<li>Specific details such as time and location of an appointment or where suspected fraud use occurred encourages action.</li>
|
||||
</ul>
|
||||
|
||||
<p>Placeholders are filled in with details, like a name or reference number, each time you send a
|
||||
message.</p>
|
||||
|
||||
<p>To add a placeholder to the template:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
<li>Add a placeholder using double brackets. For example: Hello ((first name)), your reference is
|
||||
((ref number)).</li>
|
||||
<li>Select <b class="bold">Save</b>.</li>
|
||||
<h3>To personalize your content</h3>
|
||||
<ol class="list">
|
||||
<li>Add a placeholder to your content by placing two brackets around the personalized elements.</li>
|
||||
<li>You can manually enter the personalized content or you can upload a spreadsheet with the details and let Notify do the
|
||||
work for you. See <a href="#prepare-data">data preparation</a>.</li>
|
||||
</ol>
|
||||
|
||||
<p>When you send a message you can either:</p>
|
||||
<h4>Example</h4>
|
||||
<p>To personalize with the recipient's first name and include a reference number:</p>
|
||||
<p class="padding-2 bg-base-lightest">State WIC: Hello ((first name)), your reference is ((ref number)). Please provide this number when you call 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??We’ve identified unauthorized use on your EBT account. Call the phone number on the back of your card to
|
||||
cancel or go to your local CSO for immediate replacement.))((Spanish??Hemos identificado un uso no autorizado en su
|
||||
cuenta EBT. Llame al número de teléfono que aparece en el reverso de su tarjeta para cancelarla o diríjase a su CSO
|
||||
local para que se la sustituyan inmediatamente.))</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>do this yourself</li>
|
||||
<li>upload a list of personal details and let Notify do it for you</li>
|
||||
|
||||
{# Identify your program #}
|
||||
<h2 class="padding-top-1" id="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 recipient’s preferred language or
|
||||
location, the flag to receive the content needs to be captured in its own column with a Yes or No (Y/N) flag.</p>
|
||||
|
||||
<h3 class="font-body-lg">Change the text message sender</h3>
|
||||
<h3>Example</h3>
|
||||
<table class="usa-table">
|
||||
<caption class="usa-sr-only">
|
||||
Example
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Phone number</th>
|
||||
<th scope="col">First name</th>
|
||||
<th scope="col">Last name</th>
|
||||
<th scope="col">Spanish</th>
|
||||
<th scope="col">English</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Lulu</td>
|
||||
<td>Praether</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>3:25</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>234-567-8901</td>
|
||||
<td>Sela</td>
|
||||
<td>Appel</td>
|
||||
<td>Y</td>
|
||||
<td>N</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>4:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>123-456-7890</td>
|
||||
<td>Dexter</td>
|
||||
<td>Moseley</td>
|
||||
<td>N</td>
|
||||
<td>Y</td>
|
||||
<td>November 2, 2023</td>
|
||||
<td>2:00</td>
|
||||
<td>123 Ford Rd.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>The text message sender tells your users who the message is from.</p>
|
||||
{# Preventing fraud #}
|
||||
<h2 class="padding-top-1" id="prevent-fraud">Preventing fraud</h2>
|
||||
<p>Texting fraud is ever prevalent and while we can’t eliminate the risk, we can reduce the chances that recipients will
|
||||
fall victim to fraud.</p>
|
||||
<ol class="list">
|
||||
<li>Never send unnecessary or protected private information.</li>
|
||||
<li>Remind your recipients that text messaging is not a secure means of communication and they should not send you private
|
||||
information via text.</li>
|
||||
<li>Only include links to websites and online applications that are secure.</li>
|
||||
<li>Send an introductory text letting recipients know you will be texting them and to save the number in their phone.</li>
|
||||
<li>Include an auto-response with the name of your organization and a contact phone number.</li>
|
||||
</ol>
|
||||
|
||||
<p>To change the text message sender from the default of ‘Notify.gov’:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the <b class="bold">Text message settings</b> section of the {{
|
||||
service_link(current_service, 'main.service_settings', 'settings') }} page.</li>
|
||||
<li>Select <b class="bold">Manage</b> on the <b class="bold">Text message
|
||||
senders</b> row.</li>
|
||||
<li>Select <b class="bold">Change</b> or <b class="bold">Add text message
|
||||
sender</b>.</li>
|
||||
<h3>Examples</h3>
|
||||
<ol class="list">
|
||||
<li>Introductory text:
|
||||
</br>
|
||||
<p class="padding-2 bg-base-lightest">State Transit Dept: We're piloting a new way to get important reminders to our staff. Save this number to your phone,
|
||||
you'll receive updates from us here.</p>
|
||||
</li>
|
||||
<li>Auto-response text:
|
||||
</br>
|
||||
<p class="padding-2 bg-base-lightest">State Agency: This number is unmonitored. To contact us, call us at 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 %}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>You’ll 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>It’s 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>You’ll 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('{} ×'.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 %}
|
||||
|
||||
@@ -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 we’re working on</h2>
|
||||
<h2 id="things-we-are-working-on">What we’re 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 we’ve done</h2>
|
||||
|
||||
<p>Recently we have:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
|
||||
<li></li>
|
||||
<li></li>
|
||||
|
||||
</ul> -->
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 it’s 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 it’s 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">
|
||||
|
||||
@@ -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, we’ll 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 you’re reporting an emergency.</p>
|
||||
<p>If it’s an emergency, we’ll reply within 30 minutes and update you every hour until the problem’s fixed.</p>
|
||||
<p>If your problem is not an emergency, we’ll 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 %}
|
||||
|
||||
@@ -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 we’ll 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 you’re 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 you’re 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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}"
|
||||
|
||||
|
||||
1
app/utils/govuk_frontend_jinja/__init__.py
Normal file
1
app/utils/govuk_frontend_jinja/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .templates import Environment # noqa
|
||||
30
app/utils/govuk_frontend_jinja/flask_ext.py
Normal file
30
app/utils/govuk_frontend_jinja/flask_ext.py
Normal 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
|
||||
292
app/utils/govuk_frontend_jinja/templates.py
Normal file
292
app/utils/govuk_frontend_jinja/templates.py
Normal 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")
|
||||
@@ -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
223
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,7 +3,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudfoundry = {
|
||||
source = "cloudfoundry-community/cloudfoundry"
|
||||
version = "0.15.5"
|
||||
version = "0.53.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudfoundry = {
|
||||
source = "cloudfoundry-community/cloudfoundry"
|
||||
version = "0.50.7"
|
||||
version = "0.53.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudfoundry = {
|
||||
source = "cloudfoundry-community/cloudfoundry"
|
||||
version = "~> 0.15"
|
||||
version = "0.53.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
(
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user