Compare commits
1 Commits
04-24-2025
...
ecayer-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad8bdc5fdf |
12
.ds.baseline
@@ -127,6 +127,16 @@
|
||||
}
|
||||
],
|
||||
"results": {
|
||||
".github/workflows/checks.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 68,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/assets/js/uswds.min.js": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
@@ -674,5 +684,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-04-10T19:38:31Z"
|
||||
"generated_at": "2025-02-26T18:19:37Z"
|
||||
}
|
||||
|
||||
128
.github/workflows/checks.yml
vendored
@@ -51,75 +51,73 @@ jobs:
|
||||
- name: Check coverage threshold
|
||||
run: poetry run coverage report --fail-under=90
|
||||
|
||||
# TODO FIX THIS!
|
||||
# end-to-end-tests:
|
||||
# if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
end-to-end-tests:
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
|
||||
# permissions:
|
||||
# checks: write
|
||||
# pull-requests: write
|
||||
# contents: write
|
||||
# runs-on: ubuntu-latest
|
||||
# environment: staging
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres
|
||||
# env:
|
||||
# POSTGRES_USER: user
|
||||
# POSTGRES_PASSWORD: password
|
||||
# POSTGRES_DB: test_notification_api
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# # Maps tcp port 5432 on service container to the host
|
||||
# - 5432:5432
|
||||
# redis:
|
||||
# image: redis
|
||||
# options: >-
|
||||
# --health-cmd "redis-cli ping"
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# ports:
|
||||
# # Maps tcp port 6379 on service container to the host
|
||||
# - 6379:6379
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: test_notification_api
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 6379 on service container to the host
|
||||
- 6379:6379
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: ./.github/actions/setup-project
|
||||
# - uses: jwalton/gh-find-current-pr@v1
|
||||
# id: findPr
|
||||
# - name: Check API Server availability
|
||||
# run: |
|
||||
# curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
# - name: Run Admin server
|
||||
# # If we want to log stuff and see what's broken,
|
||||
# # insert this line:
|
||||
# # tail -f admin-server.log &
|
||||
# # above make e2e-test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- name: Check API Server availability
|
||||
run: |
|
||||
curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
- name: Run Admin server
|
||||
# If we want to log stuff and see what's broken,
|
||||
# insert this line:
|
||||
# tail -f admin-server.log &
|
||||
# above make e2e-test
|
||||
|
||||
|
||||
# run: |
|
||||
# make run-flask > admin-server.log 2>&1 &
|
||||
# tail -f admin-server.log &
|
||||
# make e2e-test
|
||||
|
||||
# env:
|
||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
# SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
# DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
# ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
# ADMIN_CLIENT_USERNAME: notify-admin
|
||||
# NOTIFY_ENVIRONMENT: e2etest
|
||||
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
# NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
# VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
run: |
|
||||
make run-flask > admin-server.log 2>&1 &
|
||||
tail -f admin-server.log &
|
||||
make e2e-test
|
||||
|
||||
env:
|
||||
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: notify-admin
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
@@ -167,7 +165,7 @@ jobs:
|
||||
run: make run-flask &
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: scanning
|
||||
FEATURE_SOCKET_ENABLED: true
|
||||
FEATURE_ABOUT_PAGE_ENABLED: true
|
||||
- name: Run OWASP Baseline Scan
|
||||
uses: zaproxy/action-baseline@v0.14.0
|
||||
with:
|
||||
|
||||
19
.github/workflows/deploy-demo.yml
vendored
@@ -24,13 +24,23 @@ jobs:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
terraform/demo/**
|
||||
terraform/shared/**
|
||||
.github/workflows/deploy-demo.yml
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/demo
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
run: terraform init
|
||||
- name: Terraform apply
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/demo
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
@@ -89,7 +99,16 @@ jobs:
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-demo.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy-demo.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
|
||||
19
.github/workflows/deploy-prod.yml
vendored
@@ -24,13 +24,23 @@ jobs:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
terraform/production/**
|
||||
terraform/shared/**
|
||||
.github/workflows/deploy-prod.yml
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/production
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
run: terraform init
|
||||
- name: Terraform apply
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/production
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
@@ -89,7 +99,16 @@ jobs:
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-production.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy-prod.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
|
||||
19
.github/workflows/deploy.yml
vendored
@@ -29,13 +29,23 @@ jobs:
|
||||
terraform_version: "^1.7.5"
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
terraform/staging/**
|
||||
terraform/shared/**
|
||||
.github/workflows/deploy.yml
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/staging
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
run: terraform init
|
||||
- name: Terraform apply
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/staging
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
@@ -96,7 +106,16 @@ jobs:
|
||||
--strategy rolling
|
||||
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-staging.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
env:
|
||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
|
||||
4
.gitignore
vendored
@@ -14,10 +14,6 @@
|
||||
*.XLSX
|
||||
|
||||
## Non user files allowed to be commited
|
||||
!app/assets/pdf/best-practices-for-texting-the-public.pdf
|
||||
!app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf
|
||||
!app/assets/pdf/best-practices-section-outline.pdf
|
||||
!app/assets/pdf/standing-up-your-own-notify.pdf
|
||||
!app/assets/pdf/tcpa_overview.pdf
|
||||
!app/assets/pdf/investing-notifications-tts-public-benefits-memo.pdf
|
||||
!app/assets/pdf/out-of-pilot-announcement.pdf
|
||||
|
||||
@@ -8,7 +8,6 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
exclude: ^app/assets/pdf/.*\.pdf$
|
||||
- id: debug-statements
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
|
||||
22
Makefile
@@ -6,7 +6,6 @@ APP_VERSION_FILE = app/version.py
|
||||
|
||||
GIT_BRANCH ?= $(shell git symbolic-ref --short HEAD 2> /dev/null || echo "detached")
|
||||
GIT_COMMIT ?= $(shell git rev-parse HEAD 2> /dev/null || echo "")
|
||||
GIT_HOOKS_PATH ?= $(shell git config --global core.hooksPath || echo "")
|
||||
|
||||
VIRTUALENV_ROOT := $(shell [ -z $$VIRTUAL_ENV ] && echo $$(pwd)/venv || echo $$VIRTUAL_ENV)
|
||||
|
||||
@@ -15,8 +14,7 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
|
||||
## DEVELOPMENT
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: ## Set up everything to run the app
|
||||
make generate-version-file
|
||||
bootstrap: generate-version-file ## Set up everything to run the app
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
@@ -26,20 +24,6 @@ bootstrap: ## Set up everything to run the app
|
||||
source $(NVMSH) && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: bootstrap-with-git-hooks
|
||||
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
|
||||
make generate-version-file
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
git config --global --unset-all core.hooksPath
|
||||
poetry run pre-commit install
|
||||
git config --global core.hookspath "${GIT_HOOKS_PATH}"
|
||||
source $(NVMSH) --no-use && nvm install && npm install
|
||||
source $(NVMSH) && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: watch-frontend
|
||||
watch-frontend: ## Build frontend and watch for changes
|
||||
source $(NVMSH) && npm run watch
|
||||
@@ -69,7 +53,7 @@ generate-version-file: ## Generates the app version file
|
||||
@echo -e "__git_commit__ = \"${GIT_COMMIT}\"\n__time__ = \"${DATE}\"" > ${APP_VERSION_FILE}
|
||||
|
||||
.PHONY: test
|
||||
test: py-lint py-test js-test ## Run tests
|
||||
test: py-lint py-test js-lint js-test ## Run tests
|
||||
|
||||
.PHONY: py-lint
|
||||
py-lint: ## Run python linting scanners and black
|
||||
@@ -99,7 +83,7 @@ too-complex:
|
||||
py-test: export NEW_RELIC_ENVIRONMENT=test
|
||||
py-test: ## Run python unit tests
|
||||
poetry run coverage run -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
poetry run coverage report --fail-under=93
|
||||
poetry run coverage report --fail-under=96
|
||||
poetry run coverage html -d .coverage_cache
|
||||
|
||||
.PHONY: dead-code
|
||||
|
||||
@@ -109,7 +109,6 @@ from app.notify_client.template_statistics_api_client import template_statistics
|
||||
from app.notify_client.upload_api_client import upload_api_client
|
||||
from app.notify_client.user_api_client import user_api_client
|
||||
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
|
||||
from app.utils.api_health import is_api_down
|
||||
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
||||
from notifications_utils import logging, request_helper
|
||||
from notifications_utils.formatters import (
|
||||
@@ -146,7 +145,6 @@ def _csp(config):
|
||||
"frame-src": [
|
||||
"https://www.youtube.com",
|
||||
"https://www.youtube-nocookie.com",
|
||||
"https://www.googletagmanager.com",
|
||||
],
|
||||
"frame-ancestors": "'none'",
|
||||
"form-action": "'self'",
|
||||
@@ -159,14 +157,11 @@ def _csp(config):
|
||||
"https://www.googletagmanager.com",
|
||||
"https://www.google-analytics.com",
|
||||
"https://dap.digitalgov.gov",
|
||||
"https://cdn.socket.io",
|
||||
],
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.google-analytics.com",
|
||||
"http://localhost:6011",
|
||||
"ws://localhost:6011",
|
||||
],
|
||||
"style-src": ["'self'", asset_domain],
|
||||
"img-src": ["'self'", asset_domain, logo_domain],
|
||||
@@ -174,25 +169,14 @@ def _csp(config):
|
||||
|
||||
|
||||
def create_app(application):
|
||||
@application.after_request
|
||||
def add_csp_header(response):
|
||||
existing_csp = response.headers.get("Content-Security-Policy", "")
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
existing_csp + "; form-action 'self';"
|
||||
)
|
||||
return response
|
||||
|
||||
@application.context_processor
|
||||
def inject_feature_flags():
|
||||
# this is where feature flags can be easily added as a dictionary within context
|
||||
feature_socket_enabled = application.config.get("FEATURE_SOCKET_ENABLED", False)
|
||||
return dict(
|
||||
FEATURE_SOCKET_ENABLED=feature_socket_enabled,
|
||||
feature_about_page_enabled = application.config.get(
|
||||
"FEATURE_ABOUT_PAGE_ENABLED", False
|
||||
)
|
||||
return dict(
|
||||
FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled,
|
||||
)
|
||||
|
||||
@application.context_processor
|
||||
def inject_is_api_down():
|
||||
return {"is_api_down": is_api_down()}
|
||||
|
||||
@application.context_processor
|
||||
def inject_initial_signin_url():
|
||||
@@ -698,4 +682,4 @@ def slugify(text):
|
||||
"""
|
||||
Converts text to lowercase, replaces spaces with hyphens, and removes invalid characters.
|
||||
"""
|
||||
return re.sub(r"[^a-z0-9-]", "", re.sub(r"\s+", "-", text.lower()))
|
||||
return re.sub(r'[^a-z0-9-]', '', re.sub(r'\s+', '-', text.lower()))
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m468.547 144.82h-63.047v-121.32c0-12.958-10.542-23.5-23.5-23.5h-324c-12.958 0-23.5 10.542-23.5 23.5v84.833c.344 9.931 14.659 9.923 15 0v-33.333h341v69.82h-121.998c-14.312 0-25.954 11.574-25.954 25.8v83.174l-28.866 19.221c-5.511 3.444-5.51 12.168.001 15.611l28.865 19.22v83.174c0 14.226 11.643 25.799 25.954 25.799h121.998v71.68c0 4.687-3.813 8.5-8.5 8.5h-324c-4.687 0-8.5-3.813-8.5-8.5v-20h131.5c7.444 0 13.5-6.056 13.5-13.5v-127.999c0-7.444-6.056-13.5-13.5-13.5h-131.5v-168.021c-.344-9.931-14.659-9.923-15 0v168.021h-3.5c-7.444 0-13.5 6.056-13.5 13.5v128c0 7.444 6.056 13.5 13.5 13.5h3.5v20c0 12.958 10.542 23.5 23.5 23.5h324c12.958 0 23.5-10.542 23.5-23.5v-71.68h11.5c9.931-.344 9.923-14.659 0-15h-148.498c-6.04 0-10.954-4.845-10.954-10.799v-85.173c0-3.779-1.882-7.288-5.031-9.384l-23.492-15.643 23.492-15.644c3.15-2.098 5.031-5.606 5.031-9.384v-85.173c0-5.955 4.914-10.8 10.954-10.8h200.045c6.04 0 10.953 4.845 10.953 10.8v220.401c0 5.955-4.913 10.799-10.953 10.799h-16.436c-9.931.344-9.923 14.659 0 15h16.436c14.311 0 25.953-11.574 25.953-25.799v-220.401c0-14.226-11.643-25.8-25.953-25.8zm-256.047-129.82v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.333 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.334 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-52.333 45v-36.5c0-4.687 3.813-8.5 8.5-8.5h13.833v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.334v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.333v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h154.5c4.687 0 8.5 3.813 8.5 8.5v36.5zm-17 268.5h147v125h-147z"/><path d="m451.356 344.514c2.439-4.507 2.214-9.973-.589-14.264l-69.093-105.771c-5.242-8.361-18.107-8.359-23.35 0l-69.092 105.771c-2.803 4.291-3.028 9.756-.589 14.263s7.139 7.307 12.264 7.307h138.186c5.125 0 9.824-2.799 12.263-7.306zm-148.499-7.694 67.143-102.786 67.143 102.786z"/><path d="m362.5 266.32v31.445c.344 9.931 14.659 9.923 15 0v-31.445c-.344-9.931-14.659-9.923-15 0z"/><path d="m370 310.82c-4.143 0-7.5 3.358-7.5 7.5v2.5c.344 9.931 14.659 9.923 15 0v-2.5c0-4.142-3.357-7.5-7.5-7.5z"/><path d="m91.375 123.094h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 149.316h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 175.539h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m91.375 225.76h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 251.982h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 278.205h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m139.354 356.038c-2.93-2.929-7.678-2.929-10.607 0l-22.747 22.747-22.747-22.747c-2.93-2.929-7.678-2.929-10.607 0s-2.929 7.678 0 10.606l22.748 22.748-22.748 22.748c-2.929 2.929-2.929 7.678 0 10.606 2.93 2.929 7.677 2.929 10.607 0l22.747-22.747 22.747 22.747c2.93 2.929 7.677 2.929 10.607 0 2.929-2.929 2.929-7.678 0-10.606l-22.748-22.748 22.748-22.748c2.929-2.928 2.929-7.677 0-10.606z"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -220,7 +220,7 @@
|
||||
|
||||
var url = type === 'service'
|
||||
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
|
||||
: `/services/${currentServiceId}/daily-stats-by-user.json?timezone=${encodeURIComponent(userTimezone)}`;
|
||||
: `/services/${currentServiceId}/daily-stats-by-user.json`;
|
||||
|
||||
|
||||
return fetch(url)
|
||||
|
||||
@@ -1,69 +1,34 @@
|
||||
function announceUploadStatusFromElement() {
|
||||
const srRegion = document.getElementById('upload-status-live');
|
||||
const success = document.getElementById('upload-success');
|
||||
const error = document.getElementById('upload-error');
|
||||
|
||||
if (!srRegion) return;
|
||||
|
||||
const message = error?.textContent || success?.textContent;
|
||||
|
||||
if (message) {
|
||||
srRegion.textContent = '';
|
||||
setTimeout(() => {
|
||||
srRegion.textContent = message + '\u00A0'; // add a non-breaking space
|
||||
srRegion.focus(); // Optional
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Exported for use in tests
|
||||
function initUploadStatusAnnouncer() {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
announceUploadStatusFromElement();
|
||||
});
|
||||
}
|
||||
|
||||
(function(Modules) {
|
||||
"use strict";
|
||||
|
||||
Modules.FileUpload = function() {
|
||||
|
||||
this.submit = () => this.$form.trigger('submit');
|
||||
|
||||
this.showCancelButton = () => {
|
||||
$('.file-upload-button', this.$form).replaceWith(`
|
||||
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
|
||||
Uploading<span class="dot-anim" aria-hidden="true"></span>
|
||||
</button>
|
||||
`);
|
||||
};
|
||||
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
|
||||
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
|
||||
`);
|
||||
|
||||
this.start = function(component) {
|
||||
|
||||
this.$form = $(component);
|
||||
|
||||
this.$form.on('click', '[data-module="upload-trigger"]', function () {
|
||||
const inputId = $(this).data('file-input-id');
|
||||
const fileInput = document.getElementById(inputId);
|
||||
if (fileInput) fileInput.click();
|
||||
});
|
||||
// The label gets styled like a button and is used to hide the native file upload control. This is so that
|
||||
// users see a button that looks like the others on the site.
|
||||
|
||||
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
|
||||
|
||||
// Clear the form if the user navigates back to the page
|
||||
$(window).on("pageshow", () => this.$form[0].reset());
|
||||
|
||||
this.$form.on('change', '.file-upload-field', () => {
|
||||
this.submit();
|
||||
this.showCancelButton();
|
||||
});
|
||||
// Need to put the event on the container, not the input for it to work properly
|
||||
this.$form.on(
|
||||
'change', '.file-upload-field',
|
||||
() => this.submit() && this.showCancelButton()
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
})(window.GOVUK.Modules);
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
announceUploadStatusFromElement,
|
||||
initUploadStatusAnnouncer
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
initUploadStatusAnnouncer();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
this.$scrollableTable
|
||||
.on('scroll', this.toggleShadows)
|
||||
.on('scroll', this.maintainHeight)
|
||||
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
|
||||
|
||||
if (
|
||||
window.GOVUK.stickAtBottomWhenScrolling &&
|
||||
@@ -36,11 +37,11 @@
|
||||
|
||||
this.insertShims = () => {
|
||||
|
||||
const attributesForFocus = 'role aria-labelledby';
|
||||
const attributesForFocus = 'role aria-labelledby tabindex';
|
||||
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
|
||||
|
||||
this.$table.find('caption').attr('id', captionId);
|
||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
|
||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`);
|
||||
|
||||
this.$component
|
||||
.append(
|
||||
|
||||
@@ -13,23 +13,16 @@
|
||||
} else {
|
||||
|
||||
$submitButton.data('clicked', 'true');
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
|
||||
if ($submitButton.is('[name="Send"], [name="Schedule"]')) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000);
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let renableSubmitButton = $submitButton => () => {
|
||||
|
||||
$submitButton.data('clicked', '');
|
||||
$submitButton.prop('disabled', false);
|
||||
|
||||
};
|
||||
|
||||
$('form').on('submit', disableSubmitButtons);
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const isJobPage = window.location.pathname.includes('/jobs/');
|
||||
if (!isJobPage) return;
|
||||
|
||||
const jobEl = document.querySelector('[data-job-id]');
|
||||
const jobId = jobEl?.dataset?.jobId;
|
||||
const featureEnabled = jobEl?.dataset?.feature === 'true';
|
||||
const apiHost = jobEl?.dataset?.host;
|
||||
|
||||
if (!jobId) return;
|
||||
|
||||
if (featureEnabled) {
|
||||
const socket = io(apiHost);
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('join', { room: `job-${jobId}` });
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
socket.emit('leave', { room: `job-${jobId}` });
|
||||
});
|
||||
|
||||
const debouncedUpdate = debounce((data) => {
|
||||
updateAllJobSections();
|
||||
}, 1000);
|
||||
|
||||
socket.on('job_updated', (data) => {
|
||||
if (data.job_id !== jobId) return;
|
||||
debouncedUpdate(data);
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllJobSections() {
|
||||
const resourceEl = document.querySelector('[data-socket-update="status"]');
|
||||
const url = resourceEl?.dataset?.resource;
|
||||
|
||||
if (!url) {
|
||||
console.warn('No resource URL found for job updates');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then((res) => res.json())
|
||||
.then(({ status, counts, notifications }) => {
|
||||
const sections = {
|
||||
status: document.querySelector('[data-socket-update="status"]'),
|
||||
counts: document.querySelector('[data-socket-update="counts"]'),
|
||||
notifications: document.querySelector(
|
||||
'[data-socket-update="notifications"]'
|
||||
),
|
||||
};
|
||||
|
||||
if (status && sections.status) {
|
||||
sections.status.innerHTML = status;
|
||||
}
|
||||
if (counts && sections.counts) {
|
||||
sections.counts.innerHTML = counts;
|
||||
}
|
||||
if (notifications && sections.notifications) {
|
||||
sections.notifications.innerHTML = notifications;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error fetching job update partials:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -7,17 +7,13 @@ function showError(input, errorElement, message) {
|
||||
errorElement.textContent = message;
|
||||
}, 10);
|
||||
|
||||
if (input.type !== "radio" && input.type !== "checkbox") {
|
||||
input.classList.add("usa-input--error");
|
||||
}
|
||||
input.classList.add("usa-input--error");
|
||||
input.setAttribute("aria-describedby", errorElement.id);
|
||||
}
|
||||
|
||||
function hideError(input, errorElement) {
|
||||
errorElement.style.display = "none";
|
||||
if (input.type !== "radio" && input.type !== "checkbox") {
|
||||
input.classList.remove("usa-input--error");
|
||||
}
|
||||
input.classList.remove("usa-input--error");
|
||||
input.removeAttribute("aria-describedby");
|
||||
}
|
||||
|
||||
@@ -28,18 +24,16 @@ function getFieldLabel(input) {
|
||||
|
||||
// Attach validation logic to forms
|
||||
function attachValidation() {
|
||||
const forms = document.querySelectorAll('form[data-force-focus="True"]');
|
||||
const forms = document.querySelectorAll("form.send-one-off-form");
|
||||
forms.forEach((form) => {
|
||||
const inputs = form.querySelectorAll("input, textarea, select");
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
let isValid = true;
|
||||
let firstInvalidInput = null;
|
||||
const validatedRadioNames = new Set();
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.type === "hidden") return;
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
const errorId = input.id ? `${input.id}-error` : `${input.name}-error`;
|
||||
let errorElement = document.getElementById(errorId);
|
||||
|
||||
if (!errorElement) {
|
||||
@@ -47,24 +41,16 @@ function attachValidation() {
|
||||
errorElement.id = errorId;
|
||||
errorElement.classList.add("usa-error-message");
|
||||
errorElement.setAttribute("aria-live", "polite");
|
||||
errorElement.style.display = "none";
|
||||
if (input.type === "radio") {
|
||||
const group = form.querySelectorAll(`input[name="${input.name}"]`);
|
||||
const lastRadio = group[group.length - 1];
|
||||
lastRadio.parentElement.insertAdjacentElement("afterend", errorElement);
|
||||
} else {
|
||||
input.insertAdjacentElement("afterend", errorElement);
|
||||
}
|
||||
input.insertAdjacentElement("afterend", errorElement);
|
||||
}
|
||||
|
||||
if (input.type === "radio") {
|
||||
if (validatedRadioNames.has(input.name)) return;
|
||||
validatedRadioNames.add(input.name);
|
||||
const radioGroup = form.querySelectorAll(`input[name="${input.name}"]`);
|
||||
// Find all radio buttons with the same name
|
||||
const radioGroup = document.querySelectorAll(`input[name="${input.name}"]`);
|
||||
const isChecked = Array.from(radioGroup).some(radio => radio.checked);
|
||||
|
||||
if (!isChecked) {
|
||||
showError(input, errorElement, `Error: A selection must be made.`);
|
||||
showError(input, errorElement, `Error: ${getFieldLabel(input)} must be selected.`);
|
||||
isValid = false;
|
||||
if (!firstInvalidInput) {
|
||||
firstInvalidInput = input;
|
||||
@@ -86,22 +72,12 @@ function attachValidation() {
|
||||
});
|
||||
|
||||
inputs.forEach((input) => {
|
||||
if (input.type === "hidden") return;
|
||||
input.addEventListener("input", function () {
|
||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
||||
const errorElement = document.getElementById(errorId);
|
||||
if (errorElement && input.value.trim() !== "") {
|
||||
const errorElement = document.getElementById(`${input.id}-error`);
|
||||
if (input.value.trim() !== "" && errorElement) {
|
||||
hideError(input, errorElement);
|
||||
}
|
||||
});
|
||||
if (input.type === "radio") {
|
||||
input.addEventListener("change", function () {
|
||||
const errorElement = document.getElementById(`${input.name}-error`);
|
||||
if (errorElement) {
|
||||
hideError(input, errorElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
}
|
||||
|
||||
.sms-message-sender, .sms-message-file-name, .sms-message-scheduler, .sms-message-template, .sms-message-sender {
|
||||
margin: units(0.5) 0 0;
|
||||
margin:0.25rem 0 0;
|
||||
}
|
||||
|
||||
.sms-message-recipient {
|
||||
color: color('gray-cool-90');
|
||||
margin: units(0.5) 0 units(2);
|
||||
margin: units(1) 0 units(1);
|
||||
}
|
||||
|
||||
.sms-message-status {
|
||||
@@ -344,17 +344,3 @@ h2.recipient-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Button ellipses loading
|
||||
|
||||
.dot-anim::after {
|
||||
content: '.';
|
||||
animation: dotPulse 1.5s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dotPulse {
|
||||
0% { content: ''; }
|
||||
33% { content: '.'; }
|
||||
66% { content: '..'; }
|
||||
100% { content: '...'; }
|
||||
}
|
||||
@@ -139,6 +139,11 @@ td.table-empty-message {
|
||||
font-family: family('sans');
|
||||
}
|
||||
|
||||
.usa-dark-background .pill-item__label {
|
||||
color: white;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.pill-item.usa-link {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
@@ -193,6 +198,9 @@ td.table-empty-message {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
// border: 1px solid color('gray-cool-10');
|
||||
// padding: units(2);
|
||||
|
||||
.tick-cross-list-permissions {
|
||||
margin: units(1) 0;
|
||||
padding-left: units(2);
|
||||
@@ -249,11 +257,6 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.usa-checkbox.template-list-item.template-list-item-with-checkbox.template-list-item-without-ancestors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usa-checkbox__label-description {
|
||||
margin: units(2px) 0 units(1) units(4);
|
||||
}
|
||||
@@ -289,7 +292,6 @@ td.table-empty-message {
|
||||
margin-top: units(1);
|
||||
width: 100%;
|
||||
border: 1px solid color('gray-60');
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,31 +594,19 @@ td.table-empty-message {
|
||||
.big-number-smallest {
|
||||
font-size: units(3);
|
||||
}
|
||||
.pill-item__label {
|
||||
color: white;
|
||||
}
|
||||
&:not(.pill-item--selected):hover {
|
||||
background: color('blue-warm-70v');
|
||||
|
||||
.pill-item__label {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
&.pill-item--selected {
|
||||
.pill-item__label {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
&:hover {
|
||||
.pill-item__label {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
}
|
||||
&.pill-item--selected:hover {
|
||||
color: color('blue-60v');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Etc
|
||||
|
||||
.email-brand,
|
||||
.browse-list {
|
||||
padding: 0;
|
||||
@@ -1057,7 +1047,3 @@ nav.nav {
|
||||
.form-control-error {
|
||||
border: 4px solid #b10e1e
|
||||
}
|
||||
|
||||
.usa-site-alert .usa-alert .usa-alert__body {
|
||||
max-width: 75rem;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@forward "uswds-theme";
|
||||
@forward "uswds";
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@forward "main";
|
||||
@forward "legacy-styles";
|
||||
@forward "data-visualization";
|
||||
|
||||
@@ -88,7 +88,7 @@ class Config(object):
|
||||
],
|
||||
}
|
||||
|
||||
FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "false") == "true"
|
||||
FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true"
|
||||
|
||||
|
||||
def _s3_credentials_from_env(bucket_prefix):
|
||||
|
||||
@@ -344,7 +344,7 @@ def nl2br(value):
|
||||
html="escape",
|
||||
)
|
||||
).then(utils_nl2br)
|
||||
) # nosec
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@@ -257,7 +257,7 @@ def govuk_text_input_field_widget(
|
||||
|
||||
return Markup(
|
||||
render_template("components/components/input/template.njk", params=params)
|
||||
) # nosec
|
||||
)
|
||||
|
||||
|
||||
class GovukTextInputField(StringField):
|
||||
@@ -690,9 +690,7 @@ def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
if param_extensions:
|
||||
merge_jsonlike(params, param_extensions)
|
||||
|
||||
return Markup(
|
||||
render_template("forms/fields/checkboxes/macro.njk", params=params)
|
||||
) # nosec
|
||||
return Markup(render_template("forms/fields/checkboxes/macro.njk", params=params))
|
||||
|
||||
|
||||
def govuk_checkboxes_field_widget(
|
||||
@@ -706,7 +704,7 @@ def govuk_checkboxes_field_widget(
|
||||
f' data-field-label="{field_label}">'
|
||||
f" {checkboxes_string}"
|
||||
f"</div>"
|
||||
) # nosec
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -759,14 +757,12 @@ def govuk_checkboxes_field_widget(
|
||||
|
||||
return _wrap_in_collapsible(
|
||||
self.field_label,
|
||||
Markup(
|
||||
render_template("forms/fields/checkboxes/macro.njk", params=params)
|
||||
), # nosec
|
||||
Markup(render_template("forms/fields/checkboxes/macro.njk", params=params)),
|
||||
)
|
||||
else:
|
||||
return Markup(
|
||||
render_template("forms/fields/checkboxes/macro.njk", params=params)
|
||||
) # nosec
|
||||
)
|
||||
|
||||
|
||||
def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
@@ -809,7 +805,7 @@ def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
|
||||
return Markup(
|
||||
render_template("components/components/radios/template.njk", params=params)
|
||||
) # nosec
|
||||
)
|
||||
|
||||
|
||||
class GovukCheckboxField(BooleanField):
|
||||
|
||||
@@ -61,7 +61,7 @@ class ValidEmail:
|
||||
|
||||
|
||||
class NoCommasInPlaceHolders:
|
||||
def __init__(self, message="You cannot put commas between double parenthesis"):
|
||||
def __init__(self, message="You cannot put commas between double brackets"):
|
||||
self.message = message
|
||||
|
||||
def __call__(self, form, field):
|
||||
@@ -108,17 +108,33 @@ class OnlySMSCharacters:
|
||||
)
|
||||
if non_sms_characters:
|
||||
raise ValidationError(
|
||||
"Please remove the unaccepted character {} in your message, then save again".format(
|
||||
"You cannot use {} in {}. {} will not show up properly on everyone’s phones.".format(
|
||||
formatted_list(
|
||||
non_sms_characters,
|
||||
conjunction="and",
|
||||
conjunction="or",
|
||||
before_each="",
|
||||
after_each="",
|
||||
),
|
||||
{
|
||||
"sms": "text messages",
|
||||
}.get(self._template_type),
|
||||
("It" if len(non_sms_characters) == 1 else "They"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# class NoPlaceholders:
|
||||
|
||||
# def __init__(self, message=None):
|
||||
# self.message = message or (
|
||||
# 'You can’t use ((double brackets)) to personalize this message'
|
||||
# )
|
||||
|
||||
# def __call__(self, form, field):
|
||||
# if Field(field.data).placeholders:
|
||||
# raise ValidationError(self.message)
|
||||
|
||||
|
||||
class LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly:
|
||||
regex = re.compile(r"^[a-zA-Z0-9\s\._']+$")
|
||||
|
||||
|
||||
@@ -132,18 +132,13 @@ def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
|
||||
@user_has_permissions()
|
||||
def get_daily_stats_by_user(service_id):
|
||||
date_range = get_stats_date_range()
|
||||
days = date_range["days"]
|
||||
user_timezone = request.args.get("timezone", "UTC")
|
||||
|
||||
stats_utc = service_api_client.get_user_service_notification_statistics_by_day(
|
||||
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
user_id=current_user.id,
|
||||
start_date=date_range["start_date"],
|
||||
days=days,
|
||||
days=date_range["days"],
|
||||
)
|
||||
|
||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
|
||||
return jsonify(local_stats)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/template-usage")
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
@@ -21,23 +19,20 @@ from app.main.views.sub_navigation_dictionaries import (
|
||||
)
|
||||
from app.utils.user import user_is_logged_in
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Hook to check for feature flags
|
||||
@main.before_request
|
||||
def check_feature_flags():
|
||||
# Placeholder for future feature flag checks
|
||||
# Example:
|
||||
# if request.path.startswith("/some-feature") and not current_app.config.get("FEATURE_SOME_FEATURE_ENABLED", False):
|
||||
# abort(404)
|
||||
pass
|
||||
if request.path.startswith("/about") and not current_app.config.get(
|
||||
"FEATURE_ABOUT_PAGE_ENABLED", False
|
||||
):
|
||||
abort(404)
|
||||
|
||||
|
||||
@main.route("/test/feature-flags")
|
||||
def test_feature_flags():
|
||||
return jsonify(
|
||||
{"FEATURE_SOCKET_ENABLED": current_app.config["FEATURE_SOCKET_ENABLED"]}
|
||||
{"FEATURE_ABOUT_PAGE_ENABLED": current_app.config["FEATURE_ABOUT_PAGE_ENABLED"]}
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +44,7 @@ def index():
|
||||
return render_template(
|
||||
"views/signedout.html",
|
||||
sms_rate=CURRENT_SMS_RATE,
|
||||
counts=status_api_client.get_count_of_live_services_and_organizations()
|
||||
counts=status_api_client.get_count_of_live_services_and_organizations(),
|
||||
)
|
||||
|
||||
|
||||
@@ -263,11 +258,11 @@ def why_text_messaging():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/notify-service-ending")
|
||||
@user_is_logged_in
|
||||
def notify_service_ending():
|
||||
@main.route("/join-notify")
|
||||
def join_notify():
|
||||
return render_template(
|
||||
"views/notify-service-ending.html",
|
||||
"views/join-notify.html",
|
||||
navigation_links=about_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def accept_invite(token):
|
||||
<a href={url_for("main.sign_out")} class="usa-link">Sign out</a>
|
||||
and click the link again to accept this invite.
|
||||
"""
|
||||
) # nosec
|
||||
)
|
||||
|
||||
flash(message=message)
|
||||
|
||||
@@ -98,7 +98,7 @@ def accept_org_invite(token):
|
||||
""".format(
|
||||
current_user.email_address, url_for("main.sign_out")
|
||||
)
|
||||
) # nosec
|
||||
)
|
||||
|
||||
flash(message=message)
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from functools import partial
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
@@ -58,12 +57,8 @@ def view_job(service_id, job_id):
|
||||
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
api_host_name = os.environ.get('API_HOST_NAME')
|
||||
|
||||
return render_template(
|
||||
"views/jobs/job.html",
|
||||
api_host_name=api_host_name,
|
||||
FEATURE_SOCKET_ENABLED=current_app.config["FEATURE_SOCKET_ENABLED"],
|
||||
job=job,
|
||||
status=request.args.get("status", ""),
|
||||
updates_url=url_for(
|
||||
@@ -356,7 +351,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""total<span class="usa-sr-only">
|
||||
{"text message" if job_type == "sms" else job_type}s</span>"""
|
||||
), # nosec
|
||||
),
|
||||
"",
|
||||
job.notification_count,
|
||||
],
|
||||
@@ -364,7 +359,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""pending<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_sending, job_type)}</span>"""
|
||||
), # nosec
|
||||
),
|
||||
"pending",
|
||||
job.notifications_sending,
|
||||
],
|
||||
@@ -372,7 +367,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""delivered<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_delivered, job_type)}</span>"""
|
||||
), # nosec
|
||||
),
|
||||
"delivered",
|
||||
job.notifications_delivered,
|
||||
],
|
||||
@@ -380,7 +375,7 @@ def _get_job_counts(job):
|
||||
Markup(
|
||||
f"""failed<span class="usa-sr-only">
|
||||
{message_count_noun(job.notifications_failed, job_type)}</span>"""
|
||||
), # nosec
|
||||
),
|
||||
"failed",
|
||||
job.notifications_failed,
|
||||
],
|
||||
@@ -470,4 +465,4 @@ def get_preview_of_content(notification):
|
||||
notification["personalisation"],
|
||||
redact_missing_personalisation=True,
|
||||
).subject
|
||||
) # nosec
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import uuid
|
||||
from string import ascii_uppercase
|
||||
from zipfile import BadZipFile
|
||||
|
||||
import bleach
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
@@ -171,9 +170,7 @@ def send_messages(service_id, template_id):
|
||||
error_message = '<span class="error-message usa-error-message">'
|
||||
error_message = f"{error_message}{first_field_errors[0]}"
|
||||
error_message = f"{error_message}</span>"
|
||||
# use 'nosec' because we applied bleach.clean to strip dangerous tags
|
||||
error_message = bleach.clean(error_message)
|
||||
error_message = Markup(error_message) # nosec
|
||||
error_message = Markup(error_message)
|
||||
flash(error_message)
|
||||
column_headings = get_spreadsheet_column_headings_from_template(template)
|
||||
|
||||
@@ -481,7 +478,6 @@ def send_one_off_step(service_id, template_id, step_index):
|
||||
),
|
||||
template=template,
|
||||
form=form,
|
||||
current_placeholder=current_placeholder,
|
||||
skip_link=get_skip_link(step_index, template),
|
||||
back_link=back_link,
|
||||
link_to_upload=(
|
||||
@@ -928,10 +924,6 @@ def get_template_error_dict(exception):
|
||||
def preview_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
if not recipient:
|
||||
current_app.logger.warning(
|
||||
f"No recipient found for service {service_id}, template {template_id}. Redirecting..."
|
||||
)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".send_one_off",
|
||||
|
||||
@@ -62,7 +62,7 @@ def _get_access_token(code): # pragma: no cover
|
||||
code_param = f"code={code}"
|
||||
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
||||
headers = {"Authorization": "Bearer %s" % token}
|
||||
response = requests.post(url, headers=headers, timeout=30)
|
||||
response = requests.post(url, headers=headers)
|
||||
|
||||
response_json = response.json()
|
||||
id_token = get_id_token(response_json)
|
||||
@@ -88,7 +88,10 @@ def _get_access_token(code): # pragma: no cover
|
||||
def _get_user_email_and_uuid(access_token): # pragma: no cover
|
||||
headers = {"Authorization": "Bearer %s" % access_token}
|
||||
user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL")
|
||||
user_attributes = requests.get(user_info_url, headers=headers, timeout=30)
|
||||
user_attributes = requests.get(
|
||||
user_info_url,
|
||||
headers=headers,
|
||||
)
|
||||
user_email = user_attributes.json()["email"]
|
||||
user_uuid = user_attributes.json()["sub"]
|
||||
return user_email, user_uuid
|
||||
|
||||
@@ -17,7 +17,7 @@ def _sign_out_at_login_dot_gov():
|
||||
url = f"{base_url}{client_id}&{post_logout_redirect_uri}"
|
||||
current_app.logger.info(f"url={url}")
|
||||
|
||||
response = requests.post(url, timeout=30)
|
||||
response = requests.post(url)
|
||||
|
||||
# response = requests.post(url)
|
||||
current_app.logger.info(f"login.gov response: {response.text}")
|
||||
|
||||
@@ -56,6 +56,10 @@ def about_notify_nav():
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Join Notify",
|
||||
"link": "main.join_notify",
|
||||
},
|
||||
{
|
||||
"name": "Contact us",
|
||||
"link": "main.contact",
|
||||
|
||||
@@ -680,44 +680,42 @@ def count_content_length(service_id, template_type):
|
||||
)
|
||||
|
||||
|
||||
def _is_latin1(s):
|
||||
return bool(s.encode(encoding="latin-1", errors="strict"))
|
||||
|
||||
|
||||
def _get_content_count_error_and_message_for_template(template):
|
||||
url = "https://en.wikipedia.org/wiki/ISO/IEC_8859-1"
|
||||
if template.template_type == "sms":
|
||||
s1 = (
|
||||
"<html><body>Looks like your template may have one of these characters "
|
||||
"• ™ ∞ ≤ or ≥ or emoji, which won't save."
|
||||
)
|
||||
s2 = "<br>Please remove any unaccepted characters or emojis and try again.</body></html>"
|
||||
s1 = f"<html><body>Use of characters outside the <a href='{url}'>IEC_8859-1</a> character set may increase "
|
||||
s2 = "the message fragment count, resulting in additional charges, and these IEC_8859-1 "
|
||||
s3 = "characters may not display properly on some older mobile devices.</body></html>"
|
||||
|
||||
# Define characters that should be blocked
|
||||
BLOCKED_CHARACTERS = {"•", "™", "∞", "≤", "≥"}
|
||||
warning = ""
|
||||
try:
|
||||
_is_latin1(template.content)
|
||||
except UnicodeEncodeError:
|
||||
warning = f"{s1}{s2}{s3}"
|
||||
|
||||
def contains_blocked_characters(content):
|
||||
"""Check if the content contains explicitly blocked characters."""
|
||||
return any(c in BLOCKED_CHARACTERS for c in content)
|
||||
|
||||
# Check for blocked characters
|
||||
if contains_blocked_characters(template.content):
|
||||
warning = f"{s1}{s2}"
|
||||
return False, Markup(warning) # nosec
|
||||
|
||||
# If message is too long, return the length error
|
||||
if template.is_message_too_long():
|
||||
return True, (
|
||||
f"You have "
|
||||
f"{character_count(template.content_count_without_prefix - SMS_CHAR_COUNT_LIMIT)} "
|
||||
f"too many"
|
||||
)
|
||||
|
||||
# Show charge message as usual if no warning
|
||||
if template.placeholders:
|
||||
return False, Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
f"(not including personalization)."
|
||||
) # nosec
|
||||
|
||||
return False, Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)}."
|
||||
) # nosec
|
||||
return False, (
|
||||
Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
f"(not including personalization). {warning}"
|
||||
)
|
||||
)
|
||||
return False, (
|
||||
# Markup marks html contents safe so that they render properly. Don't use it if there is user input.
|
||||
Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)}. {warning} "
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
|
||||
@@ -28,17 +28,8 @@ def get_csv_upload(service_id, upload_id):
|
||||
return get_s3_object(*get_csv_location(service_id, upload_id))
|
||||
|
||||
|
||||
def remove_blank_lines(filedata):
|
||||
# sometimes people upload files with hundreds of blank lines at the end
|
||||
data = filedata["data"]
|
||||
cleaned_data = "\n".join(line for line in data.splitlines() if line.strip())
|
||||
filedata["data"] = cleaned_data
|
||||
return filedata
|
||||
|
||||
|
||||
def s3upload(service_id, filedata):
|
||||
|
||||
filedata = remove_blank_lines(filedata)
|
||||
upload_id = str(uuid.uuid4())
|
||||
bucket_name, file_location, access_key, secret_key, region = get_csv_location(
|
||||
service_id, upload_id
|
||||
|
||||
@@ -11,16 +11,10 @@
|
||||
{% include "components/head.html" %}
|
||||
</head>
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
<div id="upload-status-live"
|
||||
class="usa-sr-only"
|
||||
aria-live="assertive"
|
||||
tabindex="-1"
|
||||
role="status">
|
||||
</div>
|
||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||
{% block bodyStart %}
|
||||
{% block extra_javascripts_before_body %}
|
||||
<noscript><iframe sandbox src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -35,32 +29,6 @@
|
||||
|
||||
{% block header %}
|
||||
{% include 'components/usa_banner.html' %}
|
||||
{% if not is_api_down %}
|
||||
{% if current_user.is_authenticated or current_service or current_user.platform_admin %}
|
||||
<section class="usa-site-alert usa-site-alert--info" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
GSA will no longer offer the Notify.gov service after June 8th, 2025. Visit
|
||||
<a class="usa-link" href="/notify-service-ending">Notify.gov Service Ending</a> for more information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-heading" aria-label="Site alert,,,,">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
|
||||
<p class="usa-alert__text">
|
||||
Notify.gov is no longer accepting new partners.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% include 'components/header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -126,10 +94,10 @@
|
||||
{% block footer %}
|
||||
|
||||
{% if current_service and current_service.research_mode %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://tts.gsa.gov/" class="usa-link">Technology Transformation Services</a><span id="research-mode" class="research-mode">research mode</span>' %}
|
||||
{% set meta_suffix = 'Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a><span id="research-mode" class="research-mode">research mode</span>' %}
|
||||
{% else %}
|
||||
{% set commit_hash = ", Latest version: " + config['COMMIT_HASH'] %}
|
||||
{% set long_link = '<a href="https://tts.gsa.gov/" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set long_link = '<a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions" class="usa-link">Technology Transformation Services</a>' %}
|
||||
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None, thing=None) %}
|
||||
<div
|
||||
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
|
||||
{% if type == 'dangerous' %}
|
||||
role='group'
|
||||
tabindex='-1'
|
||||
{% endif %}
|
||||
{% if id %}
|
||||
id={{ id }}
|
||||
{% endif %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{#- Define common attributes we can use for both button and input types #}
|
||||
|
||||
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name | trim }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
||||
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
||||
|
||||
{#- Actually create a button... or a link! #}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% set labelHtml %}
|
||||
<label class="usa-label{%- if params.classes %} {{ params.classes }}{% endif %}"
|
||||
{%- for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}
|
||||
{%- if params.for %} for="{{ params.for }}"{% endif %}>
|
||||
{%- if params.text %} for="{{ params.text | slugify }}"{% endif %}>
|
||||
{{ params.html | safe if params.html else params.text }}
|
||||
</label>
|
||||
{% endset %}
|
||||
|
||||
@@ -27,21 +27,17 @@
|
||||
</label>
|
||||
{{ field(**{
|
||||
'class': 'file-upload-field',
|
||||
'id': field.name,
|
||||
'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e
|
||||
}) }}
|
||||
<button type="button"
|
||||
class="usa-button file-upload-button"
|
||||
data-module="upload-trigger"
|
||||
data-file-input-id="{{ field.name }}">
|
||||
{{ button_text }}
|
||||
</button>
|
||||
<label class="file-upload-button" for="{{ field.name }}">
|
||||
{{ button_text }}
|
||||
</label>
|
||||
{% if alternate_link and alternate_link_text %}
|
||||
<span class="file-upload-alternate-link">
|
||||
or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="file-upload-filename" aria-live="polite"></span>
|
||||
<label class="file-upload-filename" for="{{ field.name }}"></label>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }}
|
||||
</form>
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
class=None,
|
||||
id=None,
|
||||
module=None,
|
||||
data_kwargs={},
|
||||
data_force_focus=False
|
||||
data_kwargs={}
|
||||
) %}
|
||||
<form
|
||||
method="{{ method }}"
|
||||
@@ -20,7 +19,6 @@
|
||||
data-{{ key }}="{{ val }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data_force_focus %}data-force-focus="{{ data_force_focus }}"{% endif %}
|
||||
novalidate
|
||||
>
|
||||
{{ caller() }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% set is_about_page = request.path.startswith('/about') %}
|
||||
{% set is_join_notify_page = request.path.startswith('/join-notify') %}
|
||||
{% set is_contact_page = request.path.startswith('/contact') %}
|
||||
{% set is_information_section = is_about_page or is_contact_page %}
|
||||
{% set is_information_section = is_about_page or is_join_notify_page or is_contact_page %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set navigation = [
|
||||
@@ -33,6 +34,7 @@
|
||||
{% else %}
|
||||
{% set navigation = [
|
||||
{"href": url_for('main.about_notify'), "text": "About Notify", "active": is_about_page},
|
||||
{"href": url_for('main.join_notify'), "text": "Join Notify", "active": is_join_notify_page},
|
||||
{"href": url_for('main.contact'), "text": "Contact us", "active": is_contact_page}
|
||||
] %}
|
||||
{% endif %}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
highlight_placeholders=False,
|
||||
autofocus=False,
|
||||
autosize=False,
|
||||
colour_preview=False,
|
||||
help_link=None,
|
||||
help_link_text=None,
|
||||
width='2-3',
|
||||
@@ -12,82 +13,58 @@
|
||||
safe_error_message=False,
|
||||
rows=8,
|
||||
extra_form_group_classes='',
|
||||
placeholder='',
|
||||
input_id=None
|
||||
placeholder=''
|
||||
) %}
|
||||
{% set field_id = kwargs.id if kwargs.id is defined else field.id %}
|
||||
{% set described_by = '' %}
|
||||
{% if hint %}{% set described_by = described_by + field_id ~ '-hint' %}{% endif %}
|
||||
{% if field.errors %}
|
||||
{% if described_by %}{% set described_by = described_by + ' ' %}{% endif %}
|
||||
{% set described_by = described_by + field_id ~ '-error' %}
|
||||
{% endif %}
|
||||
|
||||
<div
|
||||
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
|
||||
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
|
||||
>
|
||||
<label class="usa-label" for="{{ field_id }}">
|
||||
{{ label or field.label.text }}
|
||||
<label class="usa-label" for="{{ field.name }}">
|
||||
{% if label %}
|
||||
{{ label }}
|
||||
{% else %}
|
||||
{{ field.label.text }}
|
||||
{% endif %}
|
||||
</label>
|
||||
|
||||
{% if hint %}
|
||||
<div class="usa-hint" id="{{ field_id }}-hint">
|
||||
<div class="usa-hint">
|
||||
{{ hint }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<span id="{{ field_id }}-error" class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
|
||||
<span id="{{ field.name}}-error" class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
|
||||
<span class="usa-sr-only">Error:</span>
|
||||
{% if not safe_error_message %}
|
||||
{{ field.errors[0] }}
|
||||
{% else %}
|
||||
{{ field.errors[0] | safe }}
|
||||
{% endif %}
|
||||
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if highlight_placeholders or autosize %}
|
||||
{%
|
||||
if highlight_placeholders or autosize
|
||||
%}
|
||||
{% set field_class = 'usa-textarea form-control-{} textbox-highlight-textbox'.format(width) %}
|
||||
{% else %}
|
||||
{% set field_class = 'usa-input' %}
|
||||
{% set field_class = 'form-control-{} {}'.format(width, 'textbox-right-aligned' if suffix else '') %}
|
||||
{% endif %}
|
||||
{% set field_class = 'form-control ' ~ field_class ~ (' form-control-error' if field.errors else '') %}
|
||||
|
||||
{% if autosize or highlight_placeholders %}
|
||||
<textarea
|
||||
id="{{ field_id }}"
|
||||
name="{{ field.name }}"
|
||||
class="{{ field_class }}"
|
||||
rows="{{ rows }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-describedby="{{ described_by.strip() }}"
|
||||
{% if field.flags.required %}required{% endif %}
|
||||
data-module="enhanced-textbox"
|
||||
data-highlight-placeholders="true"
|
||||
>{{ field._value() }}</textarea>
|
||||
{% else %}
|
||||
{% set input_type = 'tel' if 'phone' in field.label.text else 'text' %}
|
||||
<input
|
||||
id="{{ field_id }}"
|
||||
name="{{ field.name }}"
|
||||
type="{{ input_type }}"
|
||||
class="{{ field_class }}"
|
||||
value="{{ field._value() }}"
|
||||
placeholder="{{ placeholder }}"
|
||||
aria-describedby="{{ described_by.strip() }}"
|
||||
{% if field.flags.required %}required{% endif %}
|
||||
/>
|
||||
{% endif %}
|
||||
|
||||
{%
|
||||
set field_class = 'form-control ' + field_class + (
|
||||
' form-control-error' if field.errors else ''
|
||||
)
|
||||
%}
|
||||
{{ field(
|
||||
class=field_class,
|
||||
data_module='enhanced-textbox' if highlight_placeholders or autosize else '',
|
||||
data_highlight_placeholders='true' if highlight_placeholders else 'false',
|
||||
rows=rows|string,
|
||||
placeholder=placeholder,
|
||||
aria_describedby=field.name+"-error",
|
||||
required='required' if required else None,
|
||||
**kwargs
|
||||
) }}
|
||||
{% if suffix %}
|
||||
<span>{{ suffix }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if help_link and help_link_text %}
|
||||
<p class="textbox-help-link">
|
||||
<a class="usa-link" href="{{ help_link }}">{{ help_link_text }}</a>
|
||||
<a class="usa-link" href='{{ help_link }}'>{{ help_link_text }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<section class="grid-container usa-section margin-top-5 margin-bottom-5">
|
||||
<div class="grid-row flex-justify-center">
|
||||
<div class="desktop:grid-col-3 tablet:grid-col-4 mobile:grid-col-12 flex-align-center">
|
||||
<img
|
||||
class="width-tablet tablet:width-full"
|
||||
src="{{ asset_url('images/api-error.svg') }}"
|
||||
alt="Mobile phone showing error screen"/>
|
||||
</div>
|
||||
<div class="desktop:grid-col-6 tablet:grid-col-12 padding-left-0 desktop:padding-left-5">
|
||||
<h1 class="font-body-3xl line-height-sans-2 text-bold">
|
||||
There's currently a technical issue.
|
||||
</h1>
|
||||
<p class="font-body-lg line-height-sans-2">Thank you for your patience while we work on it. Notify will be back soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -7,7 +7,7 @@
|
||||
</h3>
|
||||
<div id="m-a2" class="usa-accordion__content usa-prose">
|
||||
<p class="usa-body">
|
||||
Use double parenthesis and ‘??’ to define optional content.
|
||||
Use double brackets and ‘??’ to define optional content.
|
||||
</p>
|
||||
<p class="bottom-gutter-1-3">
|
||||
For example if you only want to show something to people who are under
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
</h3>
|
||||
<div id="personalization" class="usa-accordion__content usa-prose">
|
||||
<p class="bottom-gutter-1-3">
|
||||
Use double parenthesis to personalize your message:
|
||||
Use double brackets to personalize your message:
|
||||
</p>
|
||||
{{ usaInsetText({
|
||||
"text": "Hello ((first name)), your reference is ((ref number))",
|
||||
"classes": ""})
|
||||
}}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Send a document by email
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Use double parenthesis to add a placeholder field to your template. This will contain a secure link to download the document.
|
||||
Use double brackets to add a placeholder field to your template. This will contain a secure link to download the document.
|
||||
</p>
|
||||
{{ usaInsetText({
|
||||
"text": "Download your document at: ((link_to_file))",
|
||||
|
||||
@@ -16,10 +16,7 @@
|
||||
<li>More effectively deliver program outcomes</li>
|
||||
<li>Save administrative costs</li>
|
||||
<li>Implement <a class="use-link usa-link--external"
|
||||
href="https://digital.gov/resources/delivering-digital-first-public-experience/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="21st Century IDEA (opens in a new tab)">21st Century
|
||||
href="https://digital.gov/resources/delivering-digital-first-public-experience/" target="_blank">21st Century
|
||||
IDEA</a> and other directives</li>
|
||||
</ul>
|
||||
<p>Notify.gov is an easy-to-use, web-based platform. It requires no technical expertise or system integration — users
|
||||
@@ -69,6 +66,7 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p><a href="/join-notify">See if Notify is right for you</a></p>
|
||||
<p>Notify.gov is a product of the <a href="/studio">Public Benefits Studio</a>, a product accelerator inside
|
||||
the federal government. </p>
|
||||
</section>
|
||||
|
||||
@@ -22,26 +22,16 @@
|
||||
</ul>
|
||||
<p>
|
||||
Notify.gov operates under a full three-year <a class="use-link usa-link--external"
|
||||
href="https://digital.gov/resources/an-introduction-to-ato/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Authority-to-Operate (ATO) (opens in a new tab)">Authority-to-Operate (ATO)</a>. This
|
||||
href="https://digital.gov/resources/an-introduction-to-ato/" target="_blank">Authority-to-Operate (ATO)</a>. This
|
||||
federal security authorization process leverages security
|
||||
controls provided by National Institute of Standards and Technology (NIST).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Our infrastructure runs on <a class="use-link usa-link--external"
|
||||
href="https://www.cloud.gov/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="cloud dot gov (opens in a new tab)">cloud.gov</a> and utilizes several
|
||||
Our infrastructure runs on <a class="use-link usa-link--external" href="https://www.cloud.gov/"
|
||||
target="_blank">cloud.gov</a> and utilizes several
|
||||
services through Amazon Web
|
||||
Services (AWS), including <a class="use-link usa-link--external"
|
||||
href="https://aws.amazon.com/sns/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="AWS SNS (opens in a new tab)">
|
||||
Services (AWS), including <a class="use-link usa-link--external" href="https://aws.amazon.com/sns/" target="_blank">
|
||||
AWS SNS </a> for sending SMS
|
||||
messages.
|
||||
</p>
|
||||
@@ -63,22 +53,15 @@
|
||||
</p>
|
||||
<h2>Multi-Factor Authentication</h2>
|
||||
<p>
|
||||
Notify.gov uses <a class="use-link usa-link--external"
|
||||
href="https://login.gov/what-is-login/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="Login dot gov (opens in a new tab)">Login.gov</a> for enhanced security.
|
||||
Notify.gov uses <a class="use-link usa-link--external" href="https://login.gov/what-is-login/"
|
||||
target="_blank">Login.gov</a> for enhanced security.
|
||||
Login.gov is an extra layer of security created by the government that uses multi-factor authentication and stronger
|
||||
passwords to protect your account.
|
||||
</p>
|
||||
<p>
|
||||
To access Notify.gov, users will use a Login.gov account associated with their agency (.gov) email with one of the
|
||||
<a class="use-link usa-link--external"
|
||||
href="https://login.gov/help/get-started/authentication-methods/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="login dot gov multi-factor authentication methods (opens in a new tab)"
|
||||
>multi-factor authentication
|
||||
<a class="use-link usa-link--external" href="https://login.gov/help/get-started/authentication-methods/"
|
||||
target="_blank">multi-factor authentication
|
||||
methods</a> offered through Login.gov.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
<p>
|
||||
Confusing or <a class="use-link usa-link--external"
|
||||
href="https://kffhealthnews.org/news/tougher-returned-mail-policies-add-to-medicaid-enrollment-drop/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="unreceived notifications (opens in a new tab)">unreceived notifications</a> are one of the largest barriers to people getting and keeping
|
||||
target="_blank">unreceived notifications</a> are one of the largest barriers to people getting and keeping
|
||||
benefits. The typical ways the government communicates with people often fall short. Low income households are more
|
||||
likely to experience housing instability, which means paper mail, already slow, can easily be missed.
|
||||
</p>
|
||||
@@ -26,9 +24,7 @@
|
||||
<p>
|
||||
<a class="use-link usa-link--external"
|
||||
href="https://www.pewresearch.org/internet/fact-sheet/mobile/?tabItem=64e32376-5a21-4b1d-8f8b-5f92406db984"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Pew Research shows that nearly all adults in the US have a cell phone (opens in a new tab)">Pew Research shows that nearly all adults in the US have a cell phone.</a> Reliance on smartphones
|
||||
target="_blank">Pew Research shows that nearly all adults in the US have a cell phone.</a> Reliance on smartphones
|
||||
for online access is especially common among Americans with lower household incomes and those with lower levels of
|
||||
formal education. Of those earning less than $30,000 a year, 28% say their mobile phone is the sole method to
|
||||
digitally connect.
|
||||
@@ -44,7 +40,8 @@
|
||||
</p>
|
||||
<p>
|
||||
Texting not only helps programs reach people using a nearly-universal communication method, it is a cost effective
|
||||
way to do so.
|
||||
way to do so. With Notify.gov <a href="/join-notify">you can get started for free</a>, allowing you to try out
|
||||
texting to complement your existing communications and outreach strategies.
|
||||
</p>
|
||||
<h2 id="what-texting-is-best-for">What texting is best for</h2>
|
||||
<p>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{{ page_header('About your service') }}
|
||||
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
|
||||
{{ form.name(param_extensions={"hint": {"text": "You can change this later"}}) }}
|
||||
|
||||
|
||||
@@ -20,11 +20,7 @@ Error
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
|
||||
|
||||
<div class="">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-error" class="usa-sr-only">File upload failed</span>
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
|
||||
{% if recipients.too_many_rows %}
|
||||
@@ -106,7 +102,7 @@ Error
|
||||
<div class="usa-alert usa-alert--error" role="alert">
|
||||
<div class="usa-alert__body">
|
||||
<h1 class="usa-alert__heading banner-title" data-module="track-error" data-error-type="Missing placeholder columns"
|
||||
data-error-label="{{ upload_id }}">Your column names need to match the double parenthesis in your template</h1>
|
||||
data-error-label="{{ upload_id }}">Your column names need to match the double brackets in your template</h1>
|
||||
<p class="usa-alert__text">
|
||||
Your file is missing {{ recipients.missing_column_headers | formatted_list(
|
||||
conjunction='and',
|
||||
@@ -154,7 +150,7 @@ Error
|
||||
) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="#content" class="usa-link back-to-top-link display-block margin-top-2">Back to top</a>
|
||||
<a href="#content" class="usa-link back-to-top-link">Back to top</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
|
||||
{{ template|string }}
|
||||
<div class="bottom-gutter-3-2">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-success" class="usa-sr-only">File uploaded successfully</span>
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{% if choose_time_form %}
|
||||
|
||||
@@ -101,9 +101,6 @@
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
{% endset %}
|
||||
{{ usaButton({
|
||||
"text": button_text,
|
||||
"name": button_text
|
||||
}) }}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="">
|
||||
{# Alert for users of AT #}
|
||||
<span id="upload-failed" class="usa-sr-only">File upload failed</span>
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% if row_errors|length == 1 %}
|
||||
<div class="usa-alert usa-alert--error" role="alert">
|
||||
|
||||
@@ -7,10 +7,19 @@
|
||||
{% block content_column_content %}
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p>For any questions, contact us at <a href="mailto:tts-notify@gsa.gov"
|
||||
<p>Is your organization interested in using Notify.gov? Find more information at <a href="/join-notify">Join
|
||||
Notify</a> or contact us at <a href="mailto:tts-notify@gsa.gov"
|
||||
aria-label="contact us at tts-notify@gsa.gov">tts-notify@gsa.gov</a>.</p>
|
||||
<p>If you are a current Notify.gov partner and have technical issues or questions, we are available
|
||||
at <a href="mailto:notify-support@gsa.gov"
|
||||
aria-label="Email Notify for technical questions at notify-support@gsa.gov">notify-support@gsa.gov</a></p>
|
||||
<p>You can expect a response within one business day.</p>
|
||||
<div class="usa-summary-box maxw-tablet __web-inspector-hide-shortcut__" role="region"
|
||||
aria-label="For partnership inquiries">
|
||||
<div class="usa-summary-box__body">
|
||||
<div class="usa-summary-box__text">
|
||||
<p class="margin-0">If you are a current Notify.gov partner and have technical issues or questions, we are available at <a
|
||||
href="mailto:notify-support@gsa.gov"
|
||||
aria-label="Email Notify for technical questions at notify-support@gsa.gov">notify-support@gsa.gov</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{% set content_hint = 'Your service name will be added to the start of your message. You can turn this off in Settings.' %}
|
||||
{% endif %}
|
||||
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
<div class="grid-container padding-0">
|
||||
{% call form_wrapper() %}
|
||||
<div class="grid-row">
|
||||
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
|
||||
{{ form.name(param_extensions={
|
||||
"extra_form_group_classes": "margin-bottom-2",
|
||||
@@ -61,12 +61,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-row width-mobile-lg">
|
||||
<div class="grid-row width-full">
|
||||
<div class="tablet:grid-col-2 mobile-lg:grid-col-12">
|
||||
{{ page_footer('Save') }}
|
||||
</div>
|
||||
<div class="tablet:grid-col-10 mobile-lg:grid-col-12">
|
||||
<p class="usa-hint margin-top-5 tablet:margin-left-2">
|
||||
<p class="usa-hint margin-top-5 tablet:margin-left-neg-2">
|
||||
After saving, you'll have the option to send.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -23,9 +23,7 @@
|
||||
<p>
|
||||
When the <a class="use-link usa-link--external"
|
||||
href="https://www.cbpp.org/research/food-assistance/targeted-text-message-outreach-can-increase-wic-enrollment-pilots-show"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Center on Budget and Policy Priorities (opens in a new tab)">Center on Budget and Policy Priorities</a> studied WIC, they found key learnings about the
|
||||
target="_blank">Center on Budget and Policy Priorities</a> studied WIC, they found key learnings about the
|
||||
quantity of messages delivered, how people engage with messages, and how they take action.
|
||||
</p>
|
||||
|
||||
@@ -70,11 +68,8 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The <a class="use-link usa-link--external"
|
||||
href="https://codeforamerica.org/resources/texting-playbook/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Code for America's Texting Playbook (opens in a new tab)">Code for America’s Texting
|
||||
The <a class="use-link usa-link--external" href="https://codeforamerica.org/resources/texting-playbook/"
|
||||
target="_blank">Code for America’s Texting
|
||||
Playbook</a>
|
||||
reported specific learnings around appointment reminders, completing
|
||||
document submission, and maintenance reminders.
|
||||
|
||||
@@ -142,9 +142,7 @@
|
||||
<p>
|
||||
The Department of Veterans Affairs provides a <a class="use-link usa-link--external"
|
||||
href="https://github.com/department-of-veterans-affairs/va.gov-team/blob/master/products/va-notify/notification-guide.md#vanotify-notification-guide"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="helpful flow-chart (opens in a new tab)">helpful flow-chart</a> that can help you decide if a text message is needed
|
||||
target="_blank">helpful flow-chart</a> that can help you decide if a text message is needed
|
||||
for the communication problem you are trying to solve.
|
||||
</p>
|
||||
<h2>Use a hypothesis framework to plan your campaign</h2>
|
||||
|
||||
@@ -101,11 +101,8 @@
|
||||
<p class="sms-message-wrapper text-left">
|
||||
{{circle_number(7) }}<b>Dept. of Social Services:</b> Hi {{circle_number(8) }}<b>Julie,</b> Your Medicaid
|
||||
renewal is closing December 31, 2023. You can renew online at {{circle_number(9) }}<a
|
||||
class="use-link usa-link--external"
|
||||
href="https://www.application.yourstate.gov"
|
||||
target="_blank"
|
||||
rel="application.yourstate.gov"
|
||||
aria-label="Yourstate dot gov (opens in a new tab)">https://www.application.yourstate.gov</a> or {{circle_number(10) }}
|
||||
class="use-link usa-link--external" href="https://www.application.yourstate.gov" target="_blank"
|
||||
rel="application.yourstate.gov">https://www.application.yourstate.gov</a> or {{circle_number(10) }}
|
||||
<b>call the number on the back</b> of your Medicaid card.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -29,10 +29,7 @@
|
||||
{% set links = [
|
||||
{
|
||||
"p_text": 'The <a class="use-link usa-link--external" href="https://www.fcc.gov/sites/default/files/tcpa-rules.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Telephone Consumer Protection Act (TCPA) (opens in a new tab)"
|
||||
>Telephone Consumer
|
||||
target="_blank">Telephone Consumer
|
||||
Protection Act (TCPA)</a> (47 USC § 227) is the federal law that impacts how organizations are allowed to
|
||||
communicate
|
||||
in bulk with the public via telephone (including text message or SMS).'
|
||||
@@ -40,9 +37,7 @@
|
||||
{
|
||||
"p_text": 'The <a class="use-link usa-link--external"
|
||||
href="https://www.fcc.gov/document/fcc-provides-best-practices-enable-critical-health-care-coverage-calls"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="FCC has ruled (opens in a new tab)">FCC has ruled</a> that <b>Federal</b> and <b>State</b> programs are exempt from the TCPA and can
|
||||
target="_blank">FCC has ruled</a> that <b>Federal</b> and <b>State</b> programs are exempt from the TCPA and can
|
||||
send
|
||||
text messages to the public without consent if conducting official business. Without explicit mention in the ruling,
|
||||
local governments, phone carriers, or any texting intermediaries might require it.'
|
||||
@@ -57,10 +52,7 @@
|
||||
case.
|
||||
For additional questions about the enforcement of the TCPA, you can watch <a class="use-link usa-link--external"
|
||||
href="https://www.performance.gov/cx/life-experiences/facing-a-financial-shock/outputs/fcc-training/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="a recorded training on public
|
||||
benefits texting (opens in a new tab)">a recorded training on public
|
||||
target="_blank">a recorded training on public
|
||||
benefits texting</a> provided by the FCC.'
|
||||
},
|
||||
] %}
|
||||
@@ -73,9 +65,7 @@
|
||||
<h4 class="usa-collection__heading">
|
||||
<a class="usa-link {% if item.is_downloadable %}usa-link--downloadable{% else %}usa-link--external{% endif %}"
|
||||
href="{% if item.is_downloadable %}{{ asset_url(item.url_link) }}{% else %}{{ item.url_link }}{% endif %}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="{{ item.url_text }} (opens in a new tab)">
|
||||
target="_blank">
|
||||
{{ item.url_text }}
|
||||
</a>
|
||||
</h4>
|
||||
@@ -129,7 +119,6 @@
|
||||
process
|
||||
to keep your phone number list up-to-date. The FCC offers a <a class="use-link usa-link--external"
|
||||
href="https://www.fcc.gov/reassigned-numbers-database" target="_blank"
|
||||
aria-label="Reassigned Numbers Database (opens in a new tab)"
|
||||
rel="Reassigned Numbers Database">Reassigned
|
||||
Numbers Database</a> to identify phone numbers that may have been reassigned since your agency obtained them.
|
||||
</p>
|
||||
|
||||
@@ -81,10 +81,7 @@
|
||||
</ul>
|
||||
<h2>What provoking action looks like</h2>
|
||||
<p class="padding-bottom-2"><a class="use-link usa-link--external"
|
||||
href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC10002044/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Evidence shows (opens in a new tab)">Evidence
|
||||
href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC10002044/" target="_blank">Evidence
|
||||
shows</a> that employing
|
||||
behavioral science is an effective way to increase the
|
||||
likelihood of a recipient
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<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 parenthesis. For example: Hello ((first name)), your reference is ((ref number)).</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>
|
||||
</ol>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<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 parenthesis 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>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>
|
||||
</ol>
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ your recipient to manage their benefits and increase follow-through.</p>
|
||||
|
||||
<h3>To personalize your content</h3>
|
||||
<ol class="list">
|
||||
<li>Add a placeholder to your content by placing two parenthesis around the personalized elements.</li>
|
||||
<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.</li>
|
||||
</ol>
|
||||
@@ -78,7 +78,7 @@ all or part of the message contingent upon specific criteria associated with the
|
||||
|
||||
<h3>To add conditional content</h3>
|
||||
<ol class="list">
|
||||
<li>Use two parenthesis and ?? to define the conditional content.</li>
|
||||
<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.</li>
|
||||
</ol>
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
error_message_with_html=True
|
||||
) }}
|
||||
{% endif %}
|
||||
<p>
|
||||
Team members must be invited directly using their .gov email address, not from forwarded invitations. Invitation links to create a Notify.gov account are active for only 24 hours. Expired invitations can e re-sent from the Team members page.
|
||||
</p>
|
||||
|
||||
{% include 'views/manage-users/permissions.html' %}
|
||||
|
||||
|
||||
@@ -11,15 +11,10 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header("Message status") }}
|
||||
<div data-job-id="{{ job.id }}" data-feature="{{FEATURE_SOCKET_ENABLED | lower}}" data-host="{{ api_host_name }}">
|
||||
{% if not job.finished_processing %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="status"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="status"
|
||||
data-form=""
|
||||
>
|
||||
@@ -30,12 +25,8 @@
|
||||
{% endif %}
|
||||
{% if not finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="counts"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="counts"
|
||||
data-form=""
|
||||
>
|
||||
@@ -50,12 +41,8 @@
|
||||
</p>
|
||||
{% if not job.processing_finished %}
|
||||
<div
|
||||
data-module="update-content"
|
||||
data-resource="{{ updates_url }}"
|
||||
{% if FEATURE_SOCKET_ENABLED %}
|
||||
data-socket-update="notifications"
|
||||
{% else %}
|
||||
data-module="update-content"
|
||||
{% endif %}
|
||||
data-key="notifications"
|
||||
data-form=""
|
||||
>
|
||||
@@ -64,6 +51,7 @@
|
||||
{% if not job.processing_finished %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% elif error == 'message-too-long' %}
|
||||
{# the only row_errors we can get when sending one off messages is that the message is too long #}
|
||||
<div class="bottom-gutter">
|
||||
{% call banner_wrapper(type='dangerous') %}
|
||||
{% include "partials/check/message-too-long.html" %}
|
||||
@@ -76,15 +77,13 @@
|
||||
help='3' if help else 0
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<!-- <p>Placeholder: This message will be delivered to <b>400 phone numbers</b> and will use a total of <b>800 message parts</b>, leaving Washington DSHS with <b>249,200 message parts remaining</b>.</p> -->
|
||||
<h3>Does everything look good?</h3>
|
||||
{% if not error %}
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
{% endset %}
|
||||
{{ usaButton({
|
||||
"text": button_text,
|
||||
"name": button_text
|
||||
}) }}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% set page_title = "Notify.gov Service Ending" %}
|
||||
|
||||
{% block per_page_title %}{{page_title}}{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
|
||||
<section class="usa-prose">
|
||||
<h1>{{page_title}}</h1>
|
||||
<p>GSA will no longer offer the Notify.gov service after June 8th, 2025. Current partners will continue to have service access until it is fully suspended. After May 30, partners will no longer be able to send messages, and the full service will be suspended by June 8, 2025.
|
||||
</p>
|
||||
<h2>Notify.gov Status Announcement</h2>
|
||||
<p class="text-italic">Notify.gov was in beta and will no longer be offered by GSA. Operations will be suspended on or before June 8. There is a chance that the service may need to stop sooner. GSA will keep partners informed to the extent possible.</p>
|
||||
|
||||
<p class="text-italic">All Notify.gov partners will be moved to trial mode no later than May 30, and will no longer be able to send messages. Partners will be able to access their templates and data through June 8.</p>
|
||||
|
||||
<p class="text-italic">We encourage government programs to explore ways to continue utilizing technology to meet people where they are. There are several commercial tools on the market that may work for Notify.gov use cases.</p>
|
||||
|
||||
<p class="text-italic">GSA is working to avoid potential disruptions to partners’ operations and ensure a smooth transition.</p>
|
||||
|
||||
<p class="text-italic">This decision is part of GSA’s renewed focus on its original mission: to be the backbone of the federal government’s administrative operations — streamlining processes, optimizing resources, and ensuring cost-effectiveness. GSA is driven to reducing federal spend, contributing to a more flexible and streamlined government, and delivering value for the American people.</p>
|
||||
|
||||
<p class="text-italic">We wish you all the best as you continue your work. </p>
|
||||
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -11,7 +11,7 @@
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
<a class="usa-link" target="_blank" rel="noopener noreferrer" aria-label="Download live services CSV report (opens in a new tab)" href="{{ url_for('main.live_services_csv') }}">Download live services csv report</a>
|
||||
<a class="usa-link" target="_blank" href="{{ url_for('main.live_services_csv') }}">Download live services csv report</a>
|
||||
</p>
|
||||
<p>
|
||||
<a class="usa-link" href="{{ url_for('main.notifications_sent_by_service') }}">Monthly notification statuses for live services</a>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/textbox.html" import textbox %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
@@ -35,23 +34,13 @@
|
||||
{% call form_wrapper(
|
||||
class='js-stick-at-top-when-scrolling send-one-off-form' if template.template_type != 'sms' else 'send-one-off-form',
|
||||
module="autofocus",
|
||||
data_kwargs={'force-focus': True},
|
||||
data_force_focus=True
|
||||
data_kwargs={'force-focus': True}
|
||||
) %}
|
||||
<div class="grid-row">
|
||||
{% set is_phone_number = form.placeholder_value.label.text == "phone number" %}
|
||||
{% set label_text = "Country code and phone number" if current_placeholder == "phone number" else current_placeholder %}
|
||||
{% set hint_text = "For example: +18885551234" if is_phone_number else None %}
|
||||
{% set extra_class = "extra-tracking" if is_phone_number else "" %}
|
||||
|
||||
<div class="grid-col-12 {{ extra_class }}">
|
||||
{{ textbox(
|
||||
form.placeholder_value,
|
||||
label=label_text,
|
||||
hint=hint_text,
|
||||
extra_form_group_classes=extra_class,
|
||||
param_extensions={"id": "phone-number", "classes": ""}
|
||||
) }}
|
||||
{% set extra_class = "extra-tracking" if form.placeholder_value.label.text == "phone number" else "" %}
|
||||
{% set placeholder_id = "phone number" if form.placeholder_value.label.text == "phone number" else "" %}
|
||||
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
||||
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
|
||||
</div>
|
||||
{% if skip_link or link_to_upload %}
|
||||
<div class="grid-col-12 margin-top-1">
|
||||
|
||||
@@ -7,77 +7,40 @@
|
||||
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
||||
|
||||
{% block service_page_title %}
|
||||
Upload your bulk-sending spreadsheet
|
||||
Upload a list of {{ 999|recipient_count_label(template.template_type) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({
|
||||
"href": url_for('main.send_one_off_step', service_id=current_service.id, template_id=template.id, step_index=0)
|
||||
}) }}
|
||||
{{ usaBackLink(params) }}
|
||||
{% endblock %}
|
||||
|
||||
{% set phone_numbers = [
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Label column A (the first column) as Phone number",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Double check it's the only column with Phone number as its label",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Make sure no duplicate phone numbers are listed in Column A",
|
||||
}
|
||||
]
|
||||
%}
|
||||
|
||||
{% set additional_data = [
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Match column labels one-to-one to the message template placeholders",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Label each additional <a class=\"usa-link\" href=\"/using-notify/how-to#personalize-content\">personalized placeholder</a> separately",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Separate each word in a column label with a space or dash, but no commas",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Fill in each personalized placeholder with the appropriate data or information",
|
||||
},
|
||||
{
|
||||
"svg_src": "#check_circle",
|
||||
"card_heading": "Fill in each <a class=\"usa-link\" href=\"/using-notify/how-to#conditional-content\">conditional placeholder</a> column with a Yes (Y) or No (N) to “answer” whether the recipient meets its criteria",
|
||||
}
|
||||
]
|
||||
%}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Upload your bulk-sending spreadsheet')}}
|
||||
<p class="font-sans-lg text-base">Organize phone numbers and information in a single spreadsheet and upload when you have multiple messages to send. Column headers in the spreadsheet will place the data in the right spots within the template that's selected.</p>
|
||||
{{ page_header('Upload a list of {}'.format(999|recipient_count_label(template.template_type))) }}
|
||||
|
||||
<div class="page-footer bottom-gutter margin-top-3">
|
||||
<div class="page-footer bottom-gutter">
|
||||
{{file_upload(
|
||||
form.file,
|
||||
allowed_file_extensions=allowed_file_extensions,
|
||||
button_text='Choose and upload a spreadsheet',
|
||||
button_text='Choose a file',
|
||||
show_errors=False
|
||||
)}}
|
||||
</div>
|
||||
<span id="upload-status-live" class="usa-sr-only" role="status" aria-live="polite" aria-atomic="true"></span>
|
||||
|
||||
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
|
||||
<h2 class="font-body-lg">Your file needs to look like this example</h2>
|
||||
<p class="hint">
|
||||
Save your file as a
|
||||
<abbr title="Comma Separated Values">CSV</abbr><span aria-hidden="true">, </span>
|
||||
<abbr title="Tab Separated Values">TSV</abbr><span aria-hidden="true">, </span>
|
||||
<abbr title="Open Document Spreadsheet">ODS</abbr>
|
||||
or Microsoft Excel spreadsheet.
|
||||
</p>
|
||||
|
||||
<div class="spreadsheet" data-module="fullscreen-table">
|
||||
{% call(item, row_number) list_table(
|
||||
example,
|
||||
caption="Example spreadsheet",
|
||||
caption="Example",
|
||||
caption_visible=False,
|
||||
field_headings=[''] + column_headings
|
||||
) %}
|
||||
@@ -87,72 +50,11 @@
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
<p class="hint">
|
||||
Each template saved in Notify is given an example <abbr title="Comma Separated Values">CSV</abbr> formatted spreadsheet like this. It's the most reliable file format for uploading your contact list. Use this example to populate your template with the right data items. Start by downloading this example for your message template. Then save it as a <abbr title="Comma Separated Values">CSV</abbr> file for bulk messaging.
|
||||
</p>
|
||||
<p class="table-show-more-link">
|
||||
<a class="usa-link display-flex margin-top-1" href="{{ url_for('.get_example_csv', service_id=current_service.id, template_id=template.id) }}" download>Download this example (<abbr title="Comma separated values">CSV</abbr>)
|
||||
<img class="margin-left-05" src="{{ asset_url('img/material-icons/download.svg') }}" alt="" />
|
||||
</a>
|
||||
<a class="usa-link" href="{{ url_for('.get_example_csv', service_id=current_service.id, template_id=template.id) }}" download>Download this example (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||
</p>
|
||||
<h2 class="font-body-lg">Your bulk-sending spreadsheet checklist</h2>
|
||||
<div class="grid-container margin-top-2 padding-0">
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-12">
|
||||
<h3>Phone numbers</h3>
|
||||
<ul class="usa-icon-list">
|
||||
{% for item in phone_numbers %}
|
||||
<li class="usa-icon-list__item">
|
||||
<div class="usa-icon-list__icon text-green">
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="usa-icon">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}{{ item.svg_src }}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="usa-icon-list__content">
|
||||
{{item.card_heading | safe }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="grid-col-12">
|
||||
<h3>Additional data</h3>
|
||||
<ul class="usa-icon-list">
|
||||
{% for item in additional_data %}
|
||||
<li class="usa-icon-list__item">
|
||||
<div class="usa-icon-list__icon text-green">
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="usa-icon">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}{{ item.svg_src }}"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="usa-icon-list__content">
|
||||
{{item.card_heading | safe }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="grid-col-12">
|
||||
<h3>If an error occurs</h3>
|
||||
<ul class="usa-icon-list">
|
||||
<li class="usa-icon-list__item">
|
||||
<div class="usa-icon-list__icon text-red">
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="usa-icon">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}#error"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="usa-icon-list__content">
|
||||
If you're receiving any errors, double check your file format and make sure your data is pasted in with values only
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="font-body-lg margin-bottom-2">You are filling in this template</h2>
|
||||
<p>Template: {{ template.name }}</p>
|
||||
<h2 class="font-body-lg margin-bottom-1">Your file will populate this template:<br><span class="font-body-lg">({{ template.name }})</span></h2>
|
||||
{{ template|string }}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
label='Search by name',
|
||||
autofocus=True
|
||||
) }}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{% if has_organizations %}
|
||||
{{ form.organizations }}
|
||||
{{ sticky_page_footer('Save') }}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{{ page_header('Free text message allowance') }}
|
||||
{{ form.free_sms_allowance }}
|
||||
{{ page_footer('Save') }}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
See <a class="usa-link" href="{{ url_for(".pricing") }}">pricing</a> for the list
|
||||
of rates.
|
||||
</p>
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{{ form.enabled }}
|
||||
{{ page_footer('Save') }}
|
||||
{% endcall %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{{ page_header('Message batch limit') }}
|
||||
{{ form.message_limit }}
|
||||
{{ page_footer('Save') }}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{{ page_header('Rate limit') }}
|
||||
{{ form.rate_limit }}
|
||||
{{ page_footer('Save') }}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-10">
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{{ form.enabled(param_extensions={
|
||||
"fieldset": {
|
||||
"legend": {
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
<p>
|
||||
You may send up to 250,000 text messages during the pilot period.
|
||||
</p>
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
<!--<p>
|
||||
You have a free allowance of
|
||||
{{ '{:,}'.format(current_service.free_sms_fragment_limit) }} text messages each
|
||||
financial year.
|
||||
</p>-->
|
||||
{% call form_wrapper() %}
|
||||
{{ form.enabled }}
|
||||
{{ page_footer('Save') }}
|
||||
{% endcall %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{{ page_header('Start text messages with service name') }}
|
||||
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{{ form.enabled }}
|
||||
{{ page_footer('Save') }}
|
||||
{% endcall %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
{{ page_header('Add text message sender') }}
|
||||
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{{ form.sms_sender(param_extensions={
|
||||
"hint": {"text": "Up to 11 characters, letters, numbers and spaces only"}
|
||||
}) }}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ page_header('Change text message sender') }}
|
||||
{% call form_wrapper(data_force_focus=True) %}
|
||||
{% call form_wrapper() %}
|
||||
{% if inbound_number %}
|
||||
<p>
|
||||
<span class="bottom-gutter-1-3"> {{ sms_sender.sms_sender }} </span>
|
||||
|
||||
@@ -4,206 +4,217 @@ import usaButton %} {% block meta %}
|
||||
name="description"
|
||||
content="Notify.gov lets you send text messages to your users. Try it now if you work in federal, state, or local government."
|
||||
/>
|
||||
{% endblock %}
|
||||
{% block pageTitle %} Notify.gov
|
||||
{% if is_api_down %} - There's currently a technical issue {% endif %}
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
{% endblock %} {% block pageTitle %} Notify.gov {% endblock %} {% block main %}
|
||||
{% block beforeContent %}{% endblock %}
|
||||
|
||||
<main id="main-content" role="main">
|
||||
{% block content %}
|
||||
{% if is_api_down %}
|
||||
{% include "error/technical_difficulties.html" %}
|
||||
{% else %}
|
||||
<section class="usa-section--dark usa-hero" aria-label="Introduction">
|
||||
<div class="grid-container padding-y-4">
|
||||
<div class="grid-row grid-gap display-flex flex-align-center">
|
||||
<div class="desktop:grid-col-8 tablet:grid-col-12">
|
||||
<h1 class="font-sans-2xl usa-hero__heading">
|
||||
Reach people where they are with government-powered text messages
|
||||
</h1>
|
||||
<p class="font-sans-lg">
|
||||
Notify.gov is a text messaging service that helps federal, state,
|
||||
local, tribal and territorial governments more effectively
|
||||
communicate with the people they serve.
|
||||
</p>
|
||||
<div class="usa-button-group margin-bottom-5 flex-align-center">
|
||||
<a
|
||||
class="usa-button usa-button login-button login-button--primary margin-right-2"
|
||||
href="{{ initial_signin_url }}"
|
||||
>Sign in with
|
||||
<img
|
||||
src="{{ asset_url('images/logo-login.svg') }}"
|
||||
alt="Login.gov logo"
|
||||
/>
|
||||
</a>
|
||||
if you are an existing partner
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<section class="usa-section--dark usa-hero" aria-label="Introduction">
|
||||
<div class="grid-container padding-y-4">
|
||||
<div class="grid-row grid-gap display-flex flex-align-center">
|
||||
<div class="desktop:grid-col-8 tablet:grid-col-12">
|
||||
<h1 class="font-sans-2xl usa-hero__heading">
|
||||
Reach people where they are with government-powered text messages
|
||||
</h1>
|
||||
<p class="font-sans-lg">
|
||||
Notify.gov is a text messaging service that helps federal, state,
|
||||
local, tribal and territorial governments more effectively
|
||||
communicate with the people they serve.
|
||||
</p>
|
||||
<div class="usa-button-group margin-bottom-5 flex-align-center">
|
||||
<a
|
||||
class="usa-button usa-button login-button login-button--primary margin-right-2"
|
||||
href="{{ initial_signin_url }}"
|
||||
>Sign in with
|
||||
<img
|
||||
src="{{ asset_url('images/product/phone-text.png') }}"
|
||||
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
|
||||
src="{{ asset_url('images/logo-login.svg') }}"
|
||||
alt="Login.gov logo"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
if you are an existing partner
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
|
||||
Government texting made easy
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Notify.gov is a text messaging platform built for government agencies.
|
||||
With minimal set-up and secure, personalized messaging, you can make
|
||||
one-way texting a part of your outreach program.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-sans-xl margin-top-0">Key features</h2>
|
||||
<div class="home-cards margin-top-5">
|
||||
<ul class="usa-card-group display-flex margin-bottom-4">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/internet.svg') }}"
|
||||
alt="Globe on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Web-based</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Nothing to download or install</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/fast.svg') }}"
|
||||
alt="Stopwatch with a notification speech bubble with a star inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Fast and easy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>No technical expertise required</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/status.svg') }}"
|
||||
alt="3 status messages, 2 successes and one failure"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Track message delivery</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>See which messages were received</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="usa-card-group">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/translation.svg') }}"
|
||||
alt="Speech bubbles with the letter A and the Chinese character for language"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send in recipients' preferred language
|
||||
</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Notify.gov has support for more than 30 character sets</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/security.svg') }}"
|
||||
alt="Lock with code icon inside on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Security and privacy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Limited data retention, encryption, and multi-factor
|
||||
authentication protect user data and manage risk with <br><a href="/about/security">our security efforts</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/send.svg') }}"
|
||||
alt="Paper airplane and a notification icon with the number 1 inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send bulk, customized, one-way messages
|
||||
</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Send hundreds or thousands of individually customized messages
|
||||
with just a few clicks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
|
||||
>
|
||||
<img
|
||||
src="{{ asset_url('images/product/phone-text.png') }}"
|
||||
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
|
||||
Government texting made easy
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
Notify.gov is a text messaging platform built for government agencies.
|
||||
With minimal set-up and secure, personalized messaging, you can make
|
||||
one-way texting a part of your outreach program.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose grid-container padding-bottom-10">
|
||||
<div class="grid-row grid-gap-3">
|
||||
<a
|
||||
class="text-no-underline tablet:grid-col-4 mobile-lg:grid-col-12"
|
||||
href="mailto:tts-notify@gsa.gov"
|
||||
>
|
||||
<div class="contact-us-card">
|
||||
<div class="grid-row flex-align-center grid-gap-2">
|
||||
<div class="grid-col-auto">
|
||||
<img
|
||||
src="{{ asset_url('images/contact.svg') }}"
|
||||
alt=""
|
||||
class="height-7 width-7"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
<p class="margin-0">
|
||||
<strong>Contact us</strong><br />
|
||||
</p>
|
||||
</div>
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose padding-bottom-1"
|
||||
>
|
||||
<h2 class="font-sans-xl margin-top-0">Key features</h2>
|
||||
<div class="home-cards margin-top-5">
|
||||
<ul class="usa-card-group display-flex margin-bottom-4">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/internet.svg') }}"
|
||||
alt="Globe on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Web-based</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Nothing to download or install</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/fast.svg') }}"
|
||||
alt="Stopwatch with a notification speech bubble with a star inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Fast and easy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>No technical expertise required</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/status.svg') }}"
|
||||
alt="3 status messages, 2 successes and one failure"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Track message delivery</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>See which messages were received</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="usa-card-group">
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/translation.svg') }}"
|
||||
alt="Speech bubbles with the letter A and the Chinese character for language"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send in recipients' preferred language
|
||||
</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>Notify.gov has support for more than 30 character sets</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/security.svg') }}"
|
||||
alt="Lock with code icon inside on top of a web browser"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">Security and privacy</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Limited data retention, encryption, and multi-factor
|
||||
authentication protect user data and manage risk with <br><a href="/about/security">our security efforts</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
|
||||
<div class="usa-card__container">
|
||||
<div class="blue-bar"></div>
|
||||
<img
|
||||
src="{{ asset_url('images/send.svg') }}"
|
||||
alt="Paper airplane and a notification icon with the number 1 inside"
|
||||
/>
|
||||
<div class="usa-card__header">
|
||||
<h3 class="font-heading-md">
|
||||
Send bulk, customized, one-way messages
|
||||
</h3>
|
||||
</div>
|
||||
<div class="usa-card__body">
|
||||
<p>
|
||||
Send hundreds or thousands of individually customized messages
|
||||
with just a few clicks
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="grid-container usa-section usa-section__home usa-prose grid-container padding-bottom-10"
|
||||
>
|
||||
<h2 class="font-heading-xl margin-top-0 margin-bottom-3">
|
||||
Who can use Notify.gov?
|
||||
</h2>
|
||||
<p class="usa-body">
|
||||
All <span class="text-bold">federal</span> agencies and programs are
|
||||
eligible to use Notify.gov.
|
||||
</p>
|
||||
<p class="usa-body">
|
||||
US
|
||||
<span class="text-bold">state, local, territorial, or tribal</span>
|
||||
governments that administer or deliver federally-funded programs or
|
||||
services may qualify to use Notify.gov to communicate with applicants and
|
||||
participants in these programs.
|
||||
</p>
|
||||
<div class="grid-container margin-top-4 padding-left-0 padding-right-0">
|
||||
<div class="grid-row grid-gap-3">
|
||||
<a
|
||||
class="text-no-underline tablet:grid-col-4 mobile-lg:grid-col-12"
|
||||
href="mailto:tts-notify@gsa.gov"
|
||||
>
|
||||
<div class="contact-us-card">
|
||||
<div class="grid-row flex-align-center grid-gap-2">
|
||||
<div class="grid-col-auto">
|
||||
<img
|
||||
src="{{ asset_url('images/contact.svg') }}"
|
||||
alt=""
|
||||
class="height-7 width-7"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
<p class="margin-0">
|
||||
<strong>Contact us</strong><br />
|
||||
To learn more about becoming a partner!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_api_down():
|
||||
api_base_url = os.getenv("API_HOST_NAME")
|
||||
try:
|
||||
response = requests.get(api_base_url, timeout=2)
|
||||
is_down = response.status_code != 200
|
||||
if is_down:
|
||||
logger.warning(f"API responded with status {response.status_code} at {api_base_url}")
|
||||
return is_down
|
||||
except RequestException as e:
|
||||
logger.error(f"API down when loading homepage {e}")
|
||||
return True
|
||||
@@ -118,10 +118,9 @@ def indent_njk(s, width=4, first=False, blank=False, indentfirst=None):
|
||||
indention = " " * width
|
||||
newline = "\n"
|
||||
|
||||
# we know 'indention' and 'newline' are safe so use nosec to bypass static scan warning
|
||||
if isinstance(s, Markup):
|
||||
indention = Markup(indention) # nosec
|
||||
newline = Markup(newline) # nosec
|
||||
indention = Markup(indention)
|
||||
newline = Markup(newline)
|
||||
|
||||
s += newline # this quirk is necessary for splitlines method
|
||||
|
||||
@@ -191,7 +190,7 @@ class NunjucksUndefined(jinja2.runtime.Undefined):
|
||||
def items(self):
|
||||
return self
|
||||
|
||||
# Allow escaping. This is required when
|
||||
# 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
|
||||
|
||||
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 357 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 552 KiB After Width: | Height: | Size: 534 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 958 KiB After Width: | Height: | Size: 927 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 480 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 276 KiB |