Compare commits

..

1 Commits

Author SHA1 Message Date
ecayer
ad8bdc5fdf Create retrospective.md
Shell for summary of notifications and codebase recs.
2025-03-13 10:43:24 -07:00
211 changed files with 4393 additions and 6345 deletions

View File

@@ -127,16 +127,6 @@
}
],
"results": {
".github/actions/deploy-proxy/action.yml": [
{
"type": "Hex High Entropy String",
"filename": ".github/actions/deploy-proxy/action.yml",
"hashed_secret": "a6c13f5da3788e8d654cd24001dc79a238723248",
"is_verified": false,
"line_number": 18,
"is_secret": false
}
],
".github/workflows/checks.yml": [
{
"type": "Secret Keyword",
@@ -171,7 +161,7 @@
"filename": "app/config.py",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false,
"line_number": 123,
"line_number": 120,
"is_secret": false
}
],
@@ -409,6 +399,26 @@
"is_secret": false
}
],
"app/templates/new/components/head.html": [
{
"type": "Base64 High Entropy String",
"filename": "app/templates/new/components/head.html",
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
"is_verified": false,
"line_number": 33,
"is_secret": false
}
],
"app/templates/old/admin_template.html": [
{
"type": "Base64 High Entropy String",
"filename": "app/templates/old/admin_template.html",
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
"is_verified": false,
"line_number": 18,
"is_secret": false
}
],
"deploy-config/sandbox.yml": [
{
"type": "Secret Keyword",
@@ -642,7 +652,37 @@
"line_number": 3266,
"is_secret": false
}
],
"tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py",
"hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff",
"is_verified": false,
"line_number": 16,
"is_secret": false
}
],
"tests/notifications_utils/clients/encryption/test_encryption_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py",
"hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605",
"is_verified": false,
"line_number": 13,
"is_secret": false
}
],
"tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py",
"hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6",
"is_verified": false,
"line_number": 16,
"is_secret": false
}
]
},
"generated_at": "2025-05-22T19:03:11Z"
"generated_at": "2025-02-26T18:19:37Z"
}

View File

@@ -9,9 +9,10 @@ body:
label: Log In
options:
- label: notify-staging.app.cloud.gov resolves
- label: Clicking sign in takes you to Login.gov
- label: Can sign in via Login.gov
- label: After successful login, you are brought to the “choose service” screen
- label: Clicking sign in takes you to login page
- label: 2FA code is sent promptly
- label: 2FA code input screen appears as expected
- label: After code is entered, you are brought to the “choose service” screen
- label: Choosing a service brings you to the dashboard for that service
- type: checkboxes
@@ -71,10 +72,11 @@ body:
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that persons profile
- label: Clicking on “Remove this team member” prompts a confirmation
- label: Clicking the confirmation removes the team member
- type: checkboxes
id: sign_out
attributes:
label: Sign Out
options:
- label: Clicking “Sign Out” takes you back to the homepage and logs you out
- label: Hitting “Sign Out” takes you back to the homepage and logs you out

View File

@@ -15,7 +15,7 @@ inputs:
default: https://github.com/GSA-TTS/cg-egress-proxy.git
proxy_version:
description: git ref to be deployed
default: 1500c67157c1a7a6fbbda7a2de172b3d0a67e703
default: main
runs:
using: composite
steps:

View File

@@ -15,13 +15,7 @@ runs:
python-version: "3.12.3"
- name: Install poetry
shell: bash
run: pip install poetry==2.1.3
- name: Install poetry export plugin
shell: bash
run: poetry self add poetry-plugin-export
- name: Downgrade virtualenv to compatible version
shell: bash
run: pip install "virtualenv<20.30"
run: pip install poetry==1.8.5
- name: Install application dependencies
shell: bash
run: make bootstrap

View File

@@ -10,9 +10,9 @@ updates:
schedule:
interval: "daily"
assignees:
- "alexjanousekGSA"
- "A-Shumway42"
reviewers:
- "alexjanousekGSA"
- "A-Shumway42"
- package-ecosystem: "npm"
directory: "/"
schedule:

View File

@@ -118,7 +118,6 @@ jobs:
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
@@ -139,12 +138,13 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.1.0
with:
inputs: requirements.txt
ignore-vulns: |
PYSEC-2023-312
PYSEC-2024-60
PYSEC-2022-43162
- name: Run npm audit
run: make npm-audit
@@ -165,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:

View File

@@ -25,12 +25,10 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.1.0
with:
inputs: requirements.txt
ignore-vulns: |
PYSEC-2023-312
- name: Run npm audit
run: make npm-audit

View File

@@ -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 }}
@@ -42,7 +52,7 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Deploy to cloud.gov
uses: cloud-gov/cg-cli-tools@main
@@ -63,8 +73,6 @@ jobs:
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-demo.app.cloud.gov/sign-out"
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
@@ -89,11 +97,18 @@ jobs:
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
--var LOGIN_PEM="$LOGIN_PEM"
--var API_PUBLIC_URL="$API_PUBLIC_URL"
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
--strategy rolling
- name: 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 }}

View File

@@ -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 }}
@@ -42,7 +52,7 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Deploy to cloud.gov
uses: cloud-gov/cg-cli-tools@main
@@ -63,8 +73,6 @@ jobs:
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://beta.notify.gov/sign-out"
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
@@ -89,11 +97,18 @@ jobs:
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
--var LOGIN_PEM="$LOGIN_PEM"
--var API_PUBLIC_URL="$API_PUBLIC_URL"
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
--strategy rolling
- name: 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 }}

View File

@@ -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 }}
@@ -47,7 +57,7 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Deploy to cloud.gov
@@ -69,8 +79,6 @@ jobs:
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
@@ -95,12 +103,19 @@ jobs:
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
--var LOGIN_PEM="$LOGIN_PEM"
--var API_PUBLIC_URL="$API_PUBLIC_URL"
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
--strategy rolling
- name: 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 }}

5
.gitignore vendored
View File

@@ -14,11 +14,6 @@
*.XLSX
## Non user files allowed to be commited
!app/assets/pdf/wa_case_study.pdf
!app/assets/pdf/best-practices-for-texting-the-public.pdf
!app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf
!app/assets/pdf/best-practices-section-outline.pdf
!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

View File

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

View File

@@ -6,4 +6,4 @@
export NEW_RELIC_PROXY_HOST=$egress_proxy
export http_proxy=$egress_proxy
export https_proxy=$egress_proxy
export no_proxy="apps.internal,s3-fips.us-gov-west-1.amazonaws.com"
export no_proxy="apps.internal"

View File

@@ -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,27 +14,16 @@ 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
poetry sync --no-root
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
poetry run playwright install --with-deps
poetry run pre-commit install
source $(NVMSH) --no-use && nvm install && npm install
source $(NVMSH) && npm ci --no-audit
source $(NVMSH) && npm run build
.PHONY: bootstrap-with-git-hooks
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
make generate-version-file
poetry sync --no-root
poetry run playwright install --with-deps
git config --global --unset-all core.hooksPath
poetry run pre-commit install
git config --global core.hookspath "${GIT_HOOKS_PATH}"
source $(NVMSH) --no-use && nvm install && npm install
source $(NVMSH) && npm ci --no-audit
source $(NVMSH) && npm run build
.PHONY: watch-frontend
watch-frontend: ## Build frontend and watch for changes
source $(NVMSH) && npm run watch
@@ -65,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
@@ -95,18 +83,18 @@ too-complex:
py-test: export NEW_RELIC_ENVIRONMENT=test
py-test: ## Run python unit tests
poetry run coverage run -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
poetry run coverage report --fail-under=93
poetry run coverage report --fail-under=96
poetry run coverage html -d .coverage_cache
.PHONY: dead-code
dead-code: ## 60% is our aspirational goal, but currently breaks the build
poetry run vulture ./app ./notifications_utils --min-confidence=100
.PHONY: e2e-test
e2e-test: export NEW_RELIC_ENVIRONMENT=test
e2e-test: ## Run end-to-end integration tests; note that --browser webkit isn't currently working
@echo "Running E2E tests in path: $${TESTPATH:-tests/end_to_end}"
@bash -c 'DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox "$${TESTPATH:-tests/end_to_end}"'
DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
.PHONY: js-lint
js-lint: ## Run javascript linting scanners
@@ -127,7 +115,7 @@ py-lock: ## Syncs dependencies and updates lock file without performing recursiv
.PHONY: freeze-requirements
freeze-requirements: ## create static requirements.txt
poetry export --output requirements.txt
poetry export --without-hashes --format=requirements.txt > requirements.txt
.PHONY: pip-audit
pip-audit:

View File

@@ -31,7 +31,7 @@ You will need the following items:
[setting up an account](https://cloud.gov/sign-up/) (requires a `.mil`,
`.gov`, or `.fed.us` email address) and getting access to the
`notify-local-dev` and `notify-staging` spaces.
- Admin privileges and SSH access on your machine; you may need to work with
- Admin priviliges and SSH access on your machine; you may need to work with
your organization's IT support staff if you're not sure or don't currently
have this access.
@@ -308,22 +308,6 @@ you'll be set with an upgraded version of Python.
_If you're not sure about the details of your current virtual environment, you can run `poetry env info` to get more information. If you've been using `pyenv` for everything, you can also see all available virtual environments with `pyenv virtualenvs`._
#### Poetry upgrades ####
If you are doing a new project setup, then after you install poetry you need to install the export plugin
```sh
poetry self add poetry-plugin-export
```
If you are upgrading from poetry 1.8.5, you need to do this:
```sh
curl -sSL https://install.python-poetry.org | python3 - --version 2.1.3
poetry self add poetry-export-plugin
```
#### Updating the .env file for Login.gov
To configure the application for Login.gov, you will need to update the following environment variables in the .env file:

View File

@@ -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 (
@@ -141,15 +140,11 @@ navigation = {
def _csp(config):
asset_domain = config["ASSET_DOMAIN"]
logo_domain = config["LOGO_CDN_DOMAIN"]
api_public_url = config["API_PUBLIC_URL"]
api_public_ws_url = config["API_PUBLIC_WS_URL"]
return {
"default-src": ["'self'", asset_domain],
"frame-src": [
"https://www.youtube.com",
"https://www.youtube-nocookie.com",
"https://www.googletagmanager.com",
],
"frame-ancestors": "'none'",
"form-action": "'self'",
@@ -162,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",
f"{api_public_url}",
f"{api_public_ws_url}",
],
"style-src": ["'self'", asset_domain],
"img-src": ["'self'", asset_domain, logo_domain],
@@ -177,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():
@@ -701,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()))

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
(function(Modules) {
"use strict";
Modules.Autofocus = function() {
this.start = function(component) {
var $component = $(component),
forceFocus = $component.data('forceFocus');
// if the page loads with a scroll position, we can't assume the item to focus onload
// is still where users intend to start
if (($(window).scrollTop() > 0) && !forceFocus) { return; }
// See if the component itself is something we want to send focus to
var target = $component.filter('input, textarea, select');
// Otherwise look inside the component to see if there are any elements
// we want to send focus to
if (target.length === 0) {
target = $('input, textarea, select', $component);
}
target.eq(0).trigger('focus');
};
};
})(window.GOVUK.Modules);

View File

@@ -0,0 +1,29 @@
(function(Modules) {
"use strict";
let isSixDigitHex = value => value.match(/^#[0-9A-F]{6}$/i);
let colourOrWhite = value => isSixDigitHex(value) ? value : '#FFFFFF';
Modules.ColourPreview = function() {
this.start = component => {
this.$input = $(component);
this.$input.closest('.usa-form-group').append(
this.$preview = $('<span class="textbox-colour-preview"></span>')
);
this.$input
.on('input', this.update)
.trigger('input');
};
this.update = () => this.$preview.css(
'background', colourOrWhite(this.$input.val())
);
};
})(window.GOVUK.Modules);

View File

@@ -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();
}

View File

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

View File

@@ -0,0 +1,24 @@
(function(window) {
"use strict";
window.GOVUK.Modules.Homepage = function() {
this.start = function(component) {
let $component = $(component);
let iterations = 0;
let timeout = null;
$component.on('click', () => {
if (++iterations == 5) {
$component.toggleClass('product-page-intro-wrapper--alternative');
}
clearTimeout(timeout);
timeout = setTimeout(() => iterations = 0, 1500);
});
};
};
})(window);

View File

@@ -15,7 +15,6 @@
let query = normalize($searchBox.val());
let results = 0;
let $noResultsMessage = $('.js-live-search-no-results');
$targets.each(function() {
@@ -40,12 +39,6 @@
});
if (query !== '' && results === 0) {
$noResultsMessage.show();
} else {
$noResultsMessage.hide();
}
if (state === 'loaded') {
if (query !== '') {
$searchBox.attr('aria-label', $searchLabel.text().trim() + ', ' + resultsSummary(results));

View File

@@ -1,5 +1,7 @@
window.GOVUK.Frontend.initAll();
$(() => $("time.timeago").timeago());
var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init();

View File

@@ -1,100 +0,0 @@
let activeModal = null;
let lastFocusedElement = null;
function openModal(modalId) {
const wrapper = document.getElementById(modalId);
if (!wrapper) return;
const modal = wrapper.querySelector('.usa-modal, dialog');
if (!modal) return;
lastFocusedElement = document.activeElement;
wrapper.classList.remove('is-hidden');
modal.removeAttribute('aria-hidden');
modal.removeAttribute('inert');
modal.removeAttribute('hidden');
document.body.classList.add('modal-open');
// Set focus to the first focusable element inside modal
const focusTarget = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusTarget) focusTarget.focus();
modal.addEventListener('keydown', function(e) {
if (e.key !== 'Tab') return;
const focusableElements = modal.querySelectorAll(
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
});
activeModal = wrapper;
}
function closeModal() {
if (!activeModal) return;
const modal = activeModal.querySelector('.usa-modal, dialog');
if (modal) {
modal.setAttribute('aria-hidden', 'true');
modal.setAttribute('inert', '');
modal.setAttribute('hidden', '');
}
activeModal.classList.add('is-hidden');
document.body.classList.remove('modal-open');
if (lastFocusedElement) lastFocusedElement.focus();
activeModal = null;
}
function attachModalTriggers() {
document.querySelectorAll('[data-open-modal]').forEach(btn => {
btn.addEventListener('click', () => {
const modalId = btn.getAttribute('data-open-modal');
openModal(modalId);
});
});
document.querySelectorAll('[data-close-modal]').forEach(btn => {
btn.addEventListener('click', () => {
closeModal();
});
});
}
// Escape key closes modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && activeModal) {
closeModal();
}
});
// Optional: click outside modal closes it
document.addEventListener('click', (e) => {
if (activeModal && e.target.classList.contains('usa-modal-overlay')) {
closeModal();
}
});
document.addEventListener('DOMContentLoaded', () => {
attachModalTriggers();
});
// ✅ Check if we're in a Node.js environment (for Jest) before using `module.exports`
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = { closeModal, openModal, attachModalTriggers };
}

View File

@@ -1,45 +1,30 @@
(function () {
(function() {
"use strict";
const disableSubmitButtons = function (event) {
const $submitButton = $(this).find(':submit');
let disableSubmitButtons = function(event) {
var $submitButton = $(this).find(':submit');
if ($submitButton.data('clicked') == 'true') {
if ($submitButton.data('clicked') === 'true') {
event.preventDefault();
return;
}
$submitButton.data('clicked', 'true');
// Add dot animation for Send/Schedule/Cancel buttons
const buttonName = $submitButton.attr('name')?.toLowerCase();
if (["send", "schedule", "cancel"].includes(buttonName)) {
$submitButton.prop('disabled', true);
// Inject dot animation span if not already present
if ($submitButton.find('.dot-anim').length === 0) {
$submitButton.append('<span class="dot-anim" aria-hidden="true"></span>');
}
// Disable Cancel button too
const $cancelButton = $('button[name]').filter(function () {
return $(this).attr('name')?.toLowerCase() === 'cancel';
});
$cancelButton.prop('disabled', true);
setTimeout(() => {
renableSubmitButton($submitButton);
}, 10000); // fallback safety
} else {
$submitButton.data('clicked', 'true');
setTimeout(renableSubmitButton($submitButton), 1500);
}
};
const renableSubmitButton = ($submitButton) => () => {
let renableSubmitButton = $submitButton => () => {
$submitButton.data('clicked', '');
$submitButton.prop('disabled', false);
$submitButton.find('.dot-anim').remove(); // clean up if needed
};
$('form').on('submit', disableSubmitButtons);
})();

View File

@@ -38,7 +38,7 @@
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
</div>
{{/choices}}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
<input type='button' class='usa-button usa-button--outline radio-select__button--done' aria-expanded='true' value='Done' />
</div>
`),
'chosen': Hogan.compile(`

View File

@@ -1,83 +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_error', (err) => {
console.error('Socket connect_error:', err);
});
socket.on('error', (err) => {
console.error('Socket error:', err);
});
socket.on('connect', () => {
socket.emit('join', { room: `job-${jobId}` });
});
window.addEventListener('beforeunload', () => {
socket.emit('leave', { room: `job-${jobId}` });
});
const debouncedUpdate = debounce((data) => {
updateAllJobSections();
}, 1000);
socket.on('job_updated', (data) => {
if (data.job_id !== jobId) return;
debouncedUpdate(data);
});
}
function updateAllJobSections() {
const resourceEl = document.querySelector('[data-socket-update="status"]');
const url = resourceEl?.dataset?.resource;
if (!url) {
console.warn('No resource URL found for job updates');
return;
}
fetch(url)
.then((res) => res.json())
.then(({ status, counts, notifications }) => {
const sections = {
status: document.querySelector('[data-socket-update="status"]'),
counts: document.querySelector('[data-socket-update="counts"]'),
notifications: document.querySelector(
'[data-socket-update="notifications"]'
),
};
if (status && sections.status) {
sections.status.innerHTML = status;
}
if (counts && sections.counts) {
sections.counts.innerHTML = counts;
}
if (notifications && sections.notifications) {
sections.notifications.innerHTML = notifications;
}
})
.catch((err) => {
console.error('Error fetching job update partials:', err);
});
}
});

View File

@@ -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);
}
});
}
});
});
}

View File

@@ -1,8 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
if (window.uswds && typeof window.uswds.init === 'function') {
console.log("Calling USWDS init");
window.uswds.init();
} else {
console.error("USWDS not found or init is not a function");
}
});

Binary file not shown.

View File

@@ -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,43 +344,3 @@ h2.recipient-list {
}
}
}
.usa-search__input, [type=search] {
border-right: 1px solid;
height: 40px;
margin-top: units(1);
}
.usa-search .search-form__button {
height: 40px;
}
// Button ellipses loading
.dot-anim {
display: inline-block;
margin-left: 0; /* remove left margin if it exists */
padding-left: 0;
font-size: 1em;
animation: dots 1s steps(3, end) infinite;
}
/* Optional: reduce spacing by removing whitespace node */
button span.dot-anim {
margin-left: 0; /* forces no space even if white-space exists */
}
.dot-anim::after {
content: '.';
animation: dotPulse 1.5s steps(3, end) infinite;
}
@keyframes dotPulse {
0% { content: ''; }
33% { content: '.'; }
66% { content: '..'; }
100% { content: '...'; }
}
.modal-open {
overflow: hidden;
}

View File

@@ -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;
}
@@ -179,19 +184,10 @@ td.table-empty-message {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 1rem 0;
padding: 1rem;
}
.user-list-item {
border: 2px solid color('gray-5');
padding: units(2);
button, .usa-accordion__content {
background-color: white;
border: 2px solid color('gray-5');
}
button:hover, button[aria-expanded="true"] {
background-color: color('gray-5');
}
@include at-media(desktop) {
width: calc(50% - units(1));
margin-bottom: 1rem;
@@ -202,12 +198,16 @@ 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);
}
.hint {
display: block;
font-size: size('body', 'sm');
font-weight: normal;
}
@@ -257,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);
}
@@ -297,7 +292,6 @@ td.table-empty-message {
margin-top: units(1);
width: 100%;
border: 1px solid color('gray-60');
height: 40px;
}
}
@@ -600,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;
@@ -1065,7 +1047,3 @@ nav.nav {
.form-control-error {
border: 4px solid #b10e1e
}
.usa-site-alert .usa-alert .usa-alert__body {
max-width: 75rem;
}

View File

@@ -1,5 +1,5 @@
@forward "uswds-theme";
@forward "uswds";
@forward "uswds-theme-custom-styles";
@forward "main";
@forward "legacy-styles";
@forward "data-visualization";

View File

@@ -37,7 +37,6 @@ $path: '/static/images/';
// Custom overrides
.govuk-link {
font-weight: bold;
color: #005ea5;
}
// Specific to this application

View File

@@ -12,9 +12,6 @@ class Config(object):
NOTIFY_APP_NAME = "admin"
NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development")
API_HOST_NAME = getenv("API_HOST_NAME", "localhost")
API_PUBLIC_URL = getenv("API_PUBLIC_URL", "localhost")
API_PUBLIC_WS_URL = getenv("API_PUBLIC_WS_URL", "localhost")
ADMIN_BASE_URL = getenv("ADMIN_BASE_URL", "http://localhost:6012")
HEADER_COLOUR = (
"#81878b" # mix(govuk-colour("dark-grey"), govuk-colour("mid-grey"))
@@ -91,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):
@@ -133,9 +130,8 @@ class Test(Development):
WTF_CSRF_ENABLED = False
ASSET_DOMAIN = "static.example.com"
ASSET_PATH = "https://static.example.com/"
API_HOST_NAME = "http://you-forgot-to-mock-an-api-call-to"
API_PUBLIC_URL = "http://you-forgot-to-mock-an-api-call-to"
API_PUBLIC_WS_URL = "ws://you-forgot-to-mock-an-api-call-to"
REDIS_URL = "redis://you-forgot-to-mock-a-redis-call-to"
LOGO_CDN_DOMAIN = "static-logos.test.com"

View File

@@ -344,7 +344,7 @@ def nl2br(value):
html="escape",
)
).then(utils_nl2br)
) # nosec
)
return ""

View File

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

View File

@@ -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 everyones 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 cant 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\._']+$")

View File

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

View File

@@ -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"]}
)
@@ -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(),
)

View File

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

View File

@@ -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_public_url = os.environ.get("API_PUBLIC_URL")
return render_template(
"views/jobs/job.html",
api_public_url=api_public_url,
FEATURE_SOCKET_ENABLED=current_app.config["FEATURE_SOCKET_ENABLED"],
job=job,
status=request.args.get("status", ""),
updates_url=url_for(
@@ -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
)

View File

@@ -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=(
@@ -561,47 +557,24 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
)
if request.args.get("from_test"):
back_link = {
"href": {
"url": url_for(
"main.send_one_off", service_id=service_id, template_id=template.id
),
"text": "Back to message personalization",
},
"html": "Back to message personalization",
}
back_link_from_preview = {
"href": {
"url": url_for(
"main.send_one_off", service_id=service_id, template_id=template.id
),
"text": "Back to message personalization",
},
"html": "Back to message personalization",
}
# TODO: may not be required after letters code removed
back_link = url_for(
"main.send_one_off", service_id=service_id, template_id=template.id
)
back_link_from_preview = url_for(
"main.send_one_off", service_id=service_id, template_id=template.id
)
choose_time_form = None
else:
back_link = {
"href": {
"url": url_for(
"main.send_messages", service_id=service_id, template_id=template.id
),
"text": "Back to upload a file",
},
"html": "Back to upload a file",
}
back_link_from_preview = {
"href": {
"url": url_for(
"main.check_messages",
service_id=service_id,
template_id=template.id,
upload_id=upload_id,
),
"text": "Back to check messages",
},
"html": "Back to check messages",
}
back_link = url_for(
"main.send_messages", service_id=service_id, template_id=template.id
)
back_link_from_preview = url_for(
"main.check_messages",
service_id=service_id,
template_id=template.id,
upload_id=upload_id,
)
choose_time_form = ChooseTimeForm()
if preview_row < 2:
@@ -779,62 +752,31 @@ def get_back_link(
preview=False,
):
if preview:
return {
"href": {
"url": url_for(
"main.check_notification",
service_id=service_id,
template_id=template.id,
),
"text": "Back to select delivery time",
},
"html": "Back to select delivery time",
}
return url_for(
"main.check_notification",
service_id=service_id,
template_id=template.id,
)
if step_index == 0:
if should_skip_template_page(template._template):
return {
"href": {
"url": url_for(
".choose_template",
service_id=service_id,
),
"text": "Back to all templates",
},
"html": "Back to all templates",
}
return url_for(
".choose_template",
service_id=service_id,
)
else:
return {
"href": {
"url": url_for(
".view_template",
service_id=service_id,
template_id=template.id,
),
"text": "Back to confirm your template",
},
"html": "Back to confirm your template",
}
# fallback for other steps
back_to_text = (
"Back to select recipients"
if step_index == 1
else "Back to message personalization"
)
return {
"href": {
"url": url_for(
"main.send_one_off_step",
return url_for(
".view_template",
service_id=service_id,
template_id=template.id,
step_index=step_index - 1,
),
"text": back_to_text,
},
"html": back_to_text,
}
)
return url_for(
"main.send_one_off_step",
service_id=service_id,
template_id=template.id,
step_index=step_index - 1,
)
def get_skip_link(step_index, template):
@@ -934,7 +876,7 @@ def _check_notification(service_id, template_id, exception=None, **kwargs):
if (not session.get("recipient")) or not all_placeholders_in_session(
template.placeholders
):
raise PermanentRedirect(back_link["href"]["url"])
raise PermanentRedirect(back_link)
template.values = get_recipient_and_placeholders_from_session(
template.template_type
@@ -982,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",

View File

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

View File

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

View File

@@ -56,6 +56,10 @@ def about_notify_nav():
},
],
},
{
"name": "Join Notify",
"link": "main.join_notify",
},
{
"name": "Contact us",
"link": "main.contact",

View File

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

View File

@@ -139,28 +139,14 @@ def tour_step(service_id, template_id, step_index):
def _get_tour_step_back_link(service_id, template_id, step_index):
if step_index == 1:
return {
"href": {
"url": url_for(
"main.begin_tour", service_id=service_id, template_id=template_id
),
"text": "Back to tour start",
},
"html": "Back to tour start",
}
else:
return {
"href": {
"url": url_for(
"main.tour_step",
service_id=service_id,
template_id=template_id,
step_index=step_index - 1,
),
"text": "Back to previous step",
},
"html": "Back to previous step",
}
return url_for(".begin_tour", service_id=service_id, template_id=template_id)
return url_for(
".tour_step",
service_id=service_id,
template_id=template_id,
step_index=step_index - 1,
)
@main.route(
@@ -197,18 +183,12 @@ def check_tour_notification(service_id, template_id):
)
)
back_link = {
"href": {
"url": url_for(
"main.tour_step",
service_id=current_service.id,
template_id=template_id,
step_index=len(placeholders),
),
"text": "Back to previous step",
},
"html": "Back to previous step",
}
back_link = url_for(
".tour_step",
service_id=current_service.id,
template_id=template_id,
step_index=len(placeholders),
)
template.values = get_recipient_and_placeholders_from_session(
template.template_type

View File

@@ -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 = "\r\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

View File

@@ -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,37 +29,11 @@
{% 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 %}
{% block main %}
<div class="grid-container minh-mobile">
<div class="grid-container">
{% block beforeContent %}
{% block backLink %}{% endblock %}
{% 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 %}
@@ -140,7 +108,7 @@
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timeLeft">
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading font-body-lg" id="sessionTimerHeading">
<h2 class="usa-modal__heading" id="sessionTimerHeading">
Your session will end soon.
<span class="usa-sr-only">Please choose to extend your session or sign out. Your session will expire in 5 minutes or less.</span>
</h2>
@@ -176,7 +144,7 @@
{% block extra_javascripts %}
{% endblock %}
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
<script src="{{ asset_url('js/uswds.min.js') }}"></script>
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
{% endblock %}
</body>
</html>

View File

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

View File

@@ -1,3 +1,3 @@
{% macro usaBackLink(params) %}
{%- include "./template.njk" with context %}
{%- include "./template.njk" -%}
{% endmacro %}

View File

@@ -1,6 +1,4 @@
<nav class="usa-breadcrumb" aria-label="Breadcrumb">
<a href="{{ params.href.url or params.href or '#' }}" class="usa-link usa-back-link display-inline-flex {{ params.classes or '' }}"
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {% endfor %}>
{{- params.html | safe or params.href.text or 'Back' -}}
</a>
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {% endfor %}>{{ params.html | safe or params.href.text or 'Back' }}</a>
</nav>

View File

@@ -1,4 +1,5 @@
{# Determine type of element to use, if not explicitly set -#}
{% if params.element %}
{% set element = params.element | lower %}
{% else %}
@@ -9,35 +10,26 @@
{% endif %}
{% endif %}
{# Define common attributes to use across all element types -#}
{#- Define common attributes that we can use across all element types #}
{%- set commonAttributes %} class="usa-button{% if params.classes %} {{ params.classes }}{% endif %}{% if params.disabled %} usa-button--disabled{% endif %}"{% for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}{% endset %}
{# Define attributes for button/input -#}
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name | trim }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
{#- Define common attributes we can use for both button and input types #}
{# Auto-append .dot-anim span for Send/Schedule buttons -#}
{%- set isSendOrSchedule = params.name and (params.name | lower == 'send' or params.name | lower == 'schedule') -%}
{%- set textContent %}
{%- 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! #}
{%- if element == 'a' %}
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{- commonAttributes | safe }}>
{{ params.html | safe if params.html else params.text }}
{%- if isSendOrSchedule -%}<span class="dot-anim" aria-hidden="true"></span>{%- endif -%}
{%- endset %}
</a>
{# Render the appropriate element -#}
{% if element == 'a' %}
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{ commonAttributes | safe }}>
{{ textContent | safe }}
</a>
{%- elseif element == 'button' %}
<button {%- if params.value %} value="{{ params.value }}"{% endif %} {{- buttonAttributes | safe }} {{- commonAttributes | safe }} {% if params.disabled %}disabled{% endif %}>
{{ params.html | safe if params.html else params.text }}
</button>
{% elseif element == 'button' %}
<button
{% if params.value %} value="{{ params.value }}"{% endif %}
{{ buttonAttributes | safe }}
{{ commonAttributes | safe }}
{% if params.disabled %}disabled{% endif %}
>
{{ textContent | safe }}
</button>
{% elseif element == 'input' %}
<input value="{{ params.text }}" {{ buttonAttributes | safe }} {{ commonAttributes | safe }}>
{% endif %}
{%- elseif element == 'input' %}
<input value="{{ params.text }}" {{- buttonAttributes | safe }} {{- commonAttributes | safe }}>
{%- endif %}

View File

@@ -2,10 +2,9 @@
{% from "../hint/macro.njk" import usaHint %}
{% from "../label/macro.njk" import usaLabel %}
{# Generate a consistent input ID #}
{% set inputId = params.id if params.id else params.label.text | default('unknown') | slugify %}
{#- a record of other elements that we need to associate with the input using
aria-describedby for example hints or error messages -#}
{% set describedBy = params.describedBy if params.describedBy else "" %}
<div class="usa-form-group {%- if params.errorMessage %} usa-form-group--error{% endif %} {%- if params.formGroup.classes %} {{ params.formGroup.classes }}{% endif %}">
{{ usaLabel({
html: params.label.html,
@@ -13,43 +12,38 @@
classes: params.label.classes,
isPageHeading: params.label.isPageHeading,
attributes: params.label.attributes,
for: inputId
for: params.text
}) | indent(2) | trim }}
{% if params.hint %}
{% set hintId = inputId + '-hint' %}
{% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
{{ usaHint({
id: hintId,
classes: params.hint.classes,
attributes: params.hint.attributes,
html: params.hint.html,
text: params.hint.text
}) | indent(2) | trim }}
{% endif %}
{% if params.errorMessage %}
{% set errorId = inputId + '-error' %}
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
{{ usaErrorMessage({
id: errorId,
classes: params.errorMessage.classes,
attributes: params.errorMessage.attributes,
html: params.errorMessage.html,
text: params.errorMessage.text,
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
}) | indent(2) | trim }}
{% endif %}
{% if params.hint %}
{% set hintId = params.id + '-hint' %}
{% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
{{ usaHint({
id: hintId,
classes: params.hint.classes,
attributes: params.hint.attributes,
html: params.hint.html,
text: params.hint.text
}) | indent(2) | trim }}
{% endif %}
{% if params.errorMessage %}
{% set errorId = params.label.text + '-error' %}
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
{{ usaErrorMessage({
id: errorId,
classes: params.errorMessage.classes,
attributes: params.errorMessage.attributes,
html: params.errorMessage.html,
text: params.errorMessage.text,
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
}) | indent(2) | trim }}
{% endif %}
<input
class="usa-input{%- if params.classes %} {{ params.classes }}{% endif %}{%- if params.errorMessage %} usa-input--error{% endif %}"
id="{{ inputId }}"
class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}"
id="{{ params.label.text | default('unknown') | slugify }}"
name="{{ params.name }}"
type="{{ params.type | default('text') }}"
{%- if params.value %} value="{{ params.value }}"{% endif %}
{%- if describedBy %} aria-describedby="{{ describedBy | trim }}"{% endif %}
{# Uncomment below if you want to use aria-labelledby too
aria-labelledby="{{ inputId }}-label" #}
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete }}"{% endif %}
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}

View File

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

View File

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

View File

@@ -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() }}

View File

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

View File

@@ -3,7 +3,7 @@
show=False,
form=None,
label=None,
autofocus=True
autofocus=False
) %}
{%- set search_label = label or form.search.label.text %}
@@ -13,7 +13,7 @@
} %}
{% if autofocus %}
{% set _ = param_extensions.__setitem__("attributes", {"autofocus": "autofocus"}) %}
{% set x=param_extensions.__setitem__("attributes", {"data-module": "autofocus"}) %}
{% endif %}
{% if show %}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -57,7 +57,7 @@
{{notification.key_name}}
</span>
<span class="grid-col-6 api-notifications-item__meta-time">
<time datetime="{{ notification.created_at }}">
<time class="timeago" datetime="{{ notification.created_at }}">
{{ notification.created_at|format_delta }}
</time>
</span>

View File

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

View File

@@ -12,7 +12,7 @@
{% block backLink %}
{{ usaBackLink(back_link) }}
{{ usaBackLink({ "href": back_link }) }}
{% endblock %}
{% block maincolumn_content %}
@@ -21,13 +21,11 @@
{{ template|string }}
<div class="bottom-gutter-3-2">
{# Alert for users of AT #}
<span id="upload-success" class="usa-sr-only">File uploaded successfully</span>
<form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
{% if choose_time_form %}
{{ choose_time_form.scheduled_for(param_extensions={
'formGroup': {'classes': ''},
'formGroup': {'classes': 'bottom-gutter-2-3'},
'attributes': {
'data-module': 'radio-select',
'data-categories': choose_time_form.scheduled_for.categories|join(','),
@@ -39,7 +37,7 @@
{% set button_text %}
Preview
{% endset %}
{{ usaButton({ "text": button_text, "classes": "margin-top-4" }) }}
{{ usaButton({ "text": button_text }) }}
</form>
</div>

View File

@@ -13,7 +13,7 @@
{% block backLink %}
{{ usaBackLink(back_link_from_preview) }}
{{ usaBackLink({ "href": back_link_from_preview }) }}
{% endblock %}
{% block maincolumn_content %}
@@ -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 %}

View File

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

View File

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

View File

@@ -11,8 +11,7 @@
{% block backLink %}
{{ usaBackLink({
"href": url_for('main.choose_template', service_id=current_service.id, template_folder_id=template_folder_id) if template_folder_id else url_for('main.choose_template', service_id=current_service.id),
"html": "Back to all templates"
"href": url_for('main.choose_template', service_id=current_service.id, template_folder_id=template_folder_id) if template_folder_id else url_for('main.choose_template', service_id=current_service.id)
}) }}
{% endblock %}
@@ -28,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",
@@ -62,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>

View File

@@ -14,22 +14,19 @@
</h1>
{% call form_wrapper(
action=url_for('.find_services_by_name'),
class='usa-search margin-bottom-4'
) %}
<div class="display-flex flex-justify-start flex-align-end gap-1 width-full">
{{ form.search(param_extensions={
"label": {"text": "Find services by name, partial name, or service ID"},
"classes": "width-full"
}) }}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{{ usaButton({
"text": "Search",
"classes": "usa-button search-form__button"
}) }}
</div>
{% endcall %}
{% call form_wrapper(
action=url_for('.find_services_by_name'),
class='usa-search margin-bottom-4'
) %}
{{ form.search(param_extensions={
"label": {"text": "Find services by name, partial name, or service ID"}
}) }}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{{ usaButton({
"text": "Search",
"classes": "usa-button search-form__button"
}) }}
{% endcall %}
{% call form_wrapper(id='search-form' ) %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

View File

@@ -15,20 +15,17 @@
{% call form_wrapper(
action=url_for('.find_users_by_email'),
class='usa-search margin-bottom-4'
action=url_for('.find_users_by_email'),
class='usa-search margin-bottom-4'
) %}
<div class="display-flex flex-align-end gap-1 width-full">
{{ form.search(param_extensions={
"label": {"text": "Find users by email, or by partial email"},
"classes": "width-full"
"label": {"text": "Find users by email, or by partial email"}
}) }}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{{ usaButton({
"text": "Search",
"classes": "usa-button search-form__button"
"classes": "search-form__button"
}) }}
</div>
{% endcall %}
{% call form_wrapper(id='search-form' ) %}

View File

@@ -60,7 +60,7 @@
<p class="hint">This person has never logged in</p>
{% else %}
<p>Last logged in
<time datetime="{{ user.logged_in_at }}">
<time class="timeago" datetime="{{ user.logged_in_at }}">
{{ user.logged_in_at|format_delta }}
</time>
</p>

View File

@@ -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 Americas Texting
The <a class="use-link usa-link--external" href="https://codeforamerica.org/resources/texting-playbook/"
target="_blank">Code for Americas Texting
Playbook</a>
reported specific learnings around appointment reminders, completing
document submission, and maintenance reminders.

View File

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

View File

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

View File

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

View File

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

View File

@@ -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&nbsp;name)), your reference is ((ref&nbsp;number)).</li>
<li>Add a placeholder using double brackets. For example: Hello ((first&nbsp;name)), your reference is ((ref&nbsp;number)).</li>
<li>Select <b class="bold">Save</b>.</li>
</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>

View File

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

View File

@@ -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' %}

View File

@@ -11,18 +11,10 @@
{% block maincolumn_content %}
{{ page_header("Message status") }}
{% if not job.finished_processing %}
<p class="max-width-full">This page refreshes automatically to show the latest message activity delivery rates, details, and reports.<br>You can watch it in progress or check back later.</p>
{% endif %}
<div data-job-id="{{ job.id }}" data-feature="{{FEATURE_SOCKET_ENABLED | lower}}" data-host="{{ api_public_url }}">
{% if not job.finished_processing %}
<div
data-module="update-content"
data-resource="{{ updates_url }}"
{% if FEATURE_SOCKET_ENABLED %}
data-socket-update="status"
{% else %}
data-module="update-content"
{% endif %}
data-key="status"
data-form=""
>
@@ -33,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=""
>
@@ -53,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=""
>
@@ -67,6 +51,7 @@
{% if not job.processing_finished %}
</div>
{% endif %}
</div>
<div> </div>
{% endblock %}

View File

@@ -30,105 +30,78 @@
</div>
{% if show_search_box %}
<div>
<div data-module="autofocus">
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
</div>
{% endif %}
<div class="user-list">
<div class="js-live-search-no-results" style="display: none;">
<p class="usa-body margin-0">No results found</p>
</div>
{% for user in users %}
{% if user.status != 'cancelled' %}
<div class="user-list-item width-full">
<h2 class="user-list-item-heading font-body-lg margin-y-0" title="{{ user.email_address }}">
<div class="user-list-item">
<h2 class="user-list-item-heading font-body-lg margin-top-0" title="{{ user.email_address }}">
{%- if user.name -%}
<span class="heading-small live-search-relevant">{{ user.name }}</span>
{%- else -%}
<span class="heading-small live-search-relevant">{{ user.email_address }}</span>
<span class="heading-small live-search-relevant">{{ user.name }}</span>
{%- endif -%}
</h2>
<p class="margin-top-0">
{%- if user.status == 'pending' -%}
<span class="live-search-relevant">{{ user.email_address }}</span>
<span class="hint">(invited)</span>
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(invited)</span>
{%- elif user.status == 'cancelled' -%}
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(cancelled invite)</span>
{%- elif user.status == 'expired' -%}
<span class="live-search-relevant">{{ user.email_address }}</span>
<span class="hint">(expired invite)</span>
<span class="live-search-relevant">{{ user.email_address }}</span><span class="hint">(expired invite)</span>
{%- elif user.id == current_user.id -%}
<span class="live-search-relevant">{{ user.email_address }}</span>
<span class="hint">(you)</span>
{%- else -%}
<span class="live-search-relevant"></span><span class="hint">(you)</span>
{% else %}
<span class="live-search-relevant">{{ user.email_address }}</span>
{% endif %}
</p>
<div class="usa-accordion usa-accordion--bordered margin-bottom-2">
<h3 class="margin-bottom-0 usa-accordion__heading">
<button
type="button"
class="usa-accordion__button"
aria-expanded="false"
aria-controls="permissions_{{user.id}}"
>
Permissions
</button>
</h3>
<div id="permissions_{{user.id}}" class="usa-accordion__content usa-prose" hidden>
<ul class="tick-cross-list-permissions">
{% for permission, label in permissions %}
{{ tick_cross(
user.has_permission_for_service(current_service.id, permission),
label
) }}
{% endfor %}
</ul>
{% if current_service.all_template_folders %}
<p class="usa-body tick-cross-list-hint">
{% set folder_count = user.template_folders_for_service(current_service) | length %}
{% if folder_count == 0 %}
Cannot see any folders
{% elif folder_count != current_service.all_template_folders | length %}
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
{% else %}
Can see all folders
{% endif %}
</p>
{% endif %}
{% if current_service.has_permission('email_auth') %}
<p class="usa-body tick-cross-list-hint">
Signs in with
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
</p>
{% endif %}
</div>
</div>
{% if current_user.has_permissions('manage_service') %}
{% if user.status == 'pending' or user.status == 'expired' %}
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id) }}">
Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span>
</a>
{% endif %}
{% if user.status == 'expired' %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id) }}">
Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span>
</a>
{% elif user.is_editable_by(current_user) %}
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id) }}">
Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span>
</a>
{% endif %}
</h2>
<h3 class="margin-bottom-0">Permissions</h3>
<ul class="tick-cross-list-permissions">
{% for permission, label in permissions %}
{{ tick_cross(
user.has_permission_for_service(current_service.id, permission),
label
) }}
{% endfor %}
</ul>
{# only show if the service has folders #}
{% if current_service.all_template_folders %}
<p class="usa-body tick-cross-list-hint">
{% set folder_count = user.template_folders_for_service(current_service) | length %}
{% if folder_count == 0 %}
Cannot see any folders
{% elif folder_count != current_service.all_template_folders | length %}
Can see {{ folder_count }} folder{% if folder_count > 1 %}s{% endif %}
{% else %}
Can see all folders
{% endif%}
</p>
{% endif %}
{% if current_service.has_permission('email_auth') %}
<p class="usa-body tick-cross-list-hint">
Signs in with
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
</p>
{% endif %}
{% if current_service.has_permission('email_auth') %}
<p class="usa-body tick-cross-list-hint">
Signs in with
{{ user.auth_type | format_auth_type(with_indefinite_article=True) }}
</p>
{% endif %}
{% if current_user.has_permissions('manage_service') %}
{% if user.status == 'pending' or user.status == 'expired' %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id)}}">Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
{% endif %}
{% if user.status == 'expired' %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.resend_invite', service_id=current_service.id, invited_user_id=user.id)}}">Resend invite<span class="usa-sr-only"> for {{ user.email_address }}</span></a>
{% elif user.is_editable_by(current_user) %}
<a class="user-list-edit-link usa-link" href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id)}}">Change details<span class="usa-sr-only"> for {{ user.name }} {{ user.email_address }}</span></a>
{% endif %}
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% endblock %}

View File

@@ -9,7 +9,7 @@
{% endblock %}
{% block backLink %}
{{ usaBackLink(back_link) }}
{{ usaBackLink({ "href": back_link }) }}
{% endblock %}
{% block maincolumn_content %}
@@ -55,7 +55,7 @@
{% if not error %}
{% if choose_time_form %}
{{ choose_time_form.scheduled_for(param_extensions={
'formGroup': {'classes': ''},
'formGroup': {'classes': 'bottom-gutter-2-3'},
'attributes': {
'data-module': 'radio-select',
'data-categories': choose_time_form.scheduled_for.categories|join(','),
@@ -66,8 +66,8 @@
{% set button_text %}
Preview
{% endset %}
{{ usaButton({ "text": button_text, "classes": "margin-top-2" }) }}
{% endif %}
{{ usaButton({ "text": button_text }) }}
{% endif %}
</form>
</div>

View File

@@ -16,9 +16,9 @@
{% block backLink %}
{% if help %}
{{ usaBackLink(back_link) }}
{{ usaBackLink({ "href": back_link }) }}
{% else %}
{{ usaBackLink(back_link_from_preview) }}
{{ usaBackLink({ "href": back_link_from_preview }) }}
{% endif %}
{% 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,62 +77,15 @@
help='3' if help else 0
)}}" class='page-footer'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<h3 class="margin-bottom-">Does everything look good?</h3>
<!-- <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": "Cancel",
"name": "Cancel",
"classes": "usa-button--secondary",
"type": "button",
"attributes": {
"data-open-modal": "cancelModal"
}
}) }}
{{ usaButton({ "text": button_text }) }}
{% endif %}
</form>
</div>
<div
class="usa-modal"
data-module="usa-modal"
id="cancelModal"
aria-hidden="true"
role="dialog"
aria-modal="true"
aria-labelledby="cancelModalHeading"
aria-describedby="cancelModalDesc"
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading font-body-lg" id="cancelModalHeading">Are you sure you want to cancel this message?</h2>
<p id="cancelModalDesc">Your template is saved, but your message and recipient details will not be. Canceling will bring you back to the template selection page.</p>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<a href="{{ url_for('main.choose_template', service_id=current_service.id) }}" class="usa-button">Yes, cancel
<span class="usa-sr-only">and return to the template selection page</span></a>
</li>
<li class="usa-button-group__item">
<button class="usa-button usa-button--unstyled padding-105 text-center" data-close-modal type="button">
No, go back <span class="usa-sr-only">to message send</span>
</button>
</li>
</ul>
</div>
</div>
<button class="usa-button usa-modal__close" aria-label="Close this window" data-close-modal type="button">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{{ asset_url('img/sprite.svg') }}#close"></use>
</svg>
</button>
</div>
</div>
{% endblock %}

View File

@@ -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 GSAs renewed focus on its original mission: to be the backbone of the federal governments 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 %}

View File

@@ -24,13 +24,13 @@
</h1>
{% if show_search_box %}
<div>
<div data-module="autofocus">
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
</div>
{% endif %}
{% for user in users %}
<div class="user-list">
<div class="user-list">
{% for user in users %}
<div class="user-list-item">
<div class="grid-row">
<div class="grid-col-9">
@@ -60,8 +60,8 @@
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
<div class="js-stick-at-bottom-when-scrolling">
{{ usaButton({

View File

@@ -32,7 +32,7 @@
<div class="grid-row bottom-gutter">
{% for noti_type in global_stats %}
<div class="grid-col-6">
<span class="big-number-dark">
<span class="big-number-dark bottom-gutter-2-3">
<span class="big-number-number">
{{ "{:,}".format(noti_type.black_box.number) }}
</span>

Some files were not shown because too many files have changed in this diff Show More