mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-27 01:34:01 -04:00
Compare commits
8 Commits
bbe1e44366
...
notify-adm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
050ce753a2 | ||
|
|
038f9aead6 | ||
|
|
50e8e4bdb0 | ||
|
|
c05b0076fa | ||
|
|
2d8d2945b7 | ||
|
|
7c9fbe8417 | ||
|
|
4519a5a333 | ||
|
|
e600087266 |
16
.ds.baseline
16
.ds.baseline
@@ -127,6 +127,16 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"results": {
|
"results": {
|
||||||
|
".github/workflows/checks.yml": [
|
||||||
|
{
|
||||||
|
"type": "Secret Keyword",
|
||||||
|
"filename": ".github/workflows/checks.yml",
|
||||||
|
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||||
|
"is_verified": false,
|
||||||
|
"line_number": 71,
|
||||||
|
"is_secret": false
|
||||||
|
}
|
||||||
|
],
|
||||||
"app/assets/js/uswds.min.js": [
|
"app/assets/js/uswds.min.js": [
|
||||||
{
|
{
|
||||||
"type": "Secret Keyword",
|
"type": "Secret Keyword",
|
||||||
@@ -151,7 +161,7 @@
|
|||||||
"filename": "app/config.py",
|
"filename": "app/config.py",
|
||||||
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
|
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 118,
|
"line_number": 120,
|
||||||
"is_secret": false
|
"is_secret": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -517,7 +527,7 @@
|
|||||||
"filename": "tests/app/main/views/test_accept_invite.py",
|
"filename": "tests/app/main/views/test_accept_invite.py",
|
||||||
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
|
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 631,
|
"line_number": 643,
|
||||||
"is_secret": false
|
"is_secret": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -674,5 +684,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"generated_at": "2025-03-20T18:22:36Z"
|
"generated_at": "2025-01-16T21:38:03Z"
|
||||||
}
|
}
|
||||||
|
|||||||
11
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
11
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
@@ -64,17 +64,6 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: '**Accessibility:**'
|
|
||||||
- type: textarea
|
|
||||||
id: accessibility
|
|
||||||
attributes:
|
|
||||||
label: "List any specific accessibility guidance or tests that need to be considered for this user story."
|
|
||||||
description: "List what type of accessibility tests need to pass."
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: '**Notes:**'
|
value: '**Notes:**'
|
||||||
|
|||||||
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
@@ -20,10 +20,3 @@ Please enter a detailed description here.
|
|||||||
* Consideration 1
|
* Consideration 1
|
||||||
* Consideration 2
|
* Consideration 2
|
||||||
* Consideration ...
|
* Consideration ...
|
||||||
|
|
||||||
## A11y Checks (if applicable)
|
|
||||||
|
|
||||||
* Double check work is getting picked up by the automated E2E tests
|
|
||||||
* Conduct browser-based tests through [AxeDevTools](https://www.deque.com/axe/devtools/) and [WAVE](https://wave.webaim.org/)
|
|
||||||
* Review the [Manual Checklist](https://docs.google.com/document/d/192bBXStebdXWtYhZQ73qaWMJhGcuSB1W6c9YBXhWZvc/edit?usp=sharing)
|
|
||||||
* Make sure there are no linting errors in VSCode or other IDE of choice
|
|
||||||
|
|||||||
139
.github/workflows/checks.yml
vendored
139
.github/workflows/checks.yml
vendored
@@ -10,7 +10,7 @@ env:
|
|||||||
FLASK_APP: application.py
|
FLASK_APP: application.py
|
||||||
WERKZEUG_DEBUG_PIN: off
|
WERKZEUG_DEBUG_PIN: off
|
||||||
REDIS_ENABLED: 0
|
REDIS_ENABLED: 0
|
||||||
NODE_VERSION: 22.3.0
|
NODE_VERSION: 16.15.1
|
||||||
AWS_US_TOLL_FREE_NUMBER: "+18556438890"
|
AWS_US_TOLL_FREE_NUMBER: "+18556438890"
|
||||||
ADMIN_BASE_URL: http://localhost:6012
|
ADMIN_BASE_URL: http://localhost:6012
|
||||||
|
|
||||||
@@ -39,9 +39,12 @@ jobs:
|
|||||||
annotations: failed-tests
|
annotations: failed-tests
|
||||||
prnumber: ${{ steps.findPr.outputs.number }}
|
prnumber: ${{ steps.findPr.outputs.number }}
|
||||||
- name: Check imports alphabetized
|
- name: Check imports alphabetized
|
||||||
run: poetry run isort --check-only ./app ./tests
|
run: poetry run isort ./app ./tests
|
||||||
|
- name: Check pep8
|
||||||
|
run: poetry run black ./app ./tests
|
||||||
- name: Run style checks
|
- name: Run style checks
|
||||||
run: poetry run flake8 .
|
run: poetry run flake8 .
|
||||||
|
|
||||||
- name: Check dead code
|
- name: Check dead code
|
||||||
run: make dead-code
|
run: make dead-code
|
||||||
- name: Run js tests
|
- name: Run js tests
|
||||||
@@ -51,75 +54,73 @@ jobs:
|
|||||||
- name: Check coverage threshold
|
- name: Check coverage threshold
|
||||||
run: poetry run coverage report --fail-under=90
|
run: poetry run coverage report --fail-under=90
|
||||||
|
|
||||||
# TODO FIX THIS!
|
end-to-end-tests:
|
||||||
# end-to-end-tests:
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
# if: ${{ github.actor != 'dependabot[bot]' }}
|
|
||||||
|
|
||||||
# permissions:
|
permissions:
|
||||||
# checks: write
|
checks: write
|
||||||
# pull-requests: write
|
pull-requests: write
|
||||||
# contents: write
|
contents: write
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# environment: staging
|
environment: staging
|
||||||
# services:
|
services:
|
||||||
# postgres:
|
postgres:
|
||||||
# image: postgres
|
image: postgres
|
||||||
# env:
|
env:
|
||||||
# POSTGRES_USER: user
|
POSTGRES_USER: user
|
||||||
# POSTGRES_PASSWORD: password
|
POSTGRES_PASSWORD: password
|
||||||
# POSTGRES_DB: test_notification_api
|
POSTGRES_DB: test_notification_api
|
||||||
# options: >-
|
options: >-
|
||||||
# --health-cmd pg_isready
|
--health-cmd pg_isready
|
||||||
# --health-interval 10s
|
--health-interval 10s
|
||||||
# --health-timeout 5s
|
--health-timeout 5s
|
||||||
# --health-retries 5
|
--health-retries 5
|
||||||
# ports:
|
ports:
|
||||||
# # Maps tcp port 5432 on service container to the host
|
# Maps tcp port 5432 on service container to the host
|
||||||
# - 5432:5432
|
- 5432:5432
|
||||||
# redis:
|
redis:
|
||||||
# image: redis
|
image: redis
|
||||||
# options: >-
|
options: >-
|
||||||
# --health-cmd "redis-cli ping"
|
--health-cmd "redis-cli ping"
|
||||||
# --health-interval 10s
|
--health-interval 10s
|
||||||
# --health-timeout 5s
|
--health-timeout 5s
|
||||||
# --health-retries 5
|
--health-retries 5
|
||||||
# ports:
|
ports:
|
||||||
# # Maps tcp port 6379 on service container to the host
|
# Maps tcp port 6379 on service container to the host
|
||||||
# - 6379:6379
|
- 6379:6379
|
||||||
|
|
||||||
# steps:
|
steps:
|
||||||
# - uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# - uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
# - uses: jwalton/gh-find-current-pr@v1
|
- uses: jwalton/gh-find-current-pr@v1
|
||||||
# id: findPr
|
id: findPr
|
||||||
# - name: Check API Server availability
|
- name: Check API Server availability
|
||||||
# run: |
|
run: |
|
||||||
# curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||||
# - name: Run Admin server
|
- name: Run Admin server
|
||||||
# # If we want to log stuff and see what's broken,
|
# If we want to log stuff and see what's broken,
|
||||||
# # insert this line:
|
# insert this line:
|
||||||
# # tail -f admin-server.log &
|
# tail -f admin-server.log &
|
||||||
# # above make e2e-test
|
# above make e2e-test
|
||||||
|
|
||||||
|
|
||||||
# run: |
|
run: |
|
||||||
# make run-flask > admin-server.log 2>&1 &
|
make run-flask > admin-server.log 2>&1 &
|
||||||
# tail -f admin-server.log &
|
tail -f admin-server.log &
|
||||||
# make e2e-test
|
make e2e-test
|
||||||
|
|
||||||
# env:
|
|
||||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
|
||||||
# SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
|
||||||
# DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
|
||||||
# ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
|
||||||
# ADMIN_CLIENT_USERNAME: notify-admin
|
|
||||||
# NOTIFY_ENVIRONMENT: e2etest
|
|
||||||
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
|
||||||
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
|
||||||
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
|
||||||
# NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
|
||||||
# VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
|
||||||
|
|
||||||
|
env:
|
||||||
|
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||||
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||||
|
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||||
|
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||||
|
ADMIN_CLIENT_USERNAME: notify-admin
|
||||||
|
NOTIFY_ENVIRONMENT: e2etest
|
||||||
|
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||||
|
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||||
|
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||||
|
NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||||
|
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||||
validate-new-relic-config:
|
validate-new-relic-config:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: staging
|
environment: staging
|
||||||
@@ -141,7 +142,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
- name: Create requirements.txt
|
- name: Create requirements.txt
|
||||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
- uses: pypa/gh-action-pip-audit@v1.0.8
|
||||||
with:
|
with:
|
||||||
inputs: requirements.txt
|
inputs: requirements.txt
|
||||||
ignore-vulns: |
|
ignore-vulns: |
|
||||||
@@ -168,7 +169,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NOTIFY_ENVIRONMENT: scanning
|
NOTIFY_ENVIRONMENT: scanning
|
||||||
- name: Run OWASP Baseline Scan
|
- name: Run OWASP Baseline Scan
|
||||||
uses: zaproxy/action-baseline@v0.14.0
|
uses: zaproxy/action-baseline@v0.9.0
|
||||||
with:
|
with:
|
||||||
docker_name: "ghcr.io/zaproxy/zaproxy:weekly"
|
docker_name: "ghcr.io/zaproxy/zaproxy:weekly"
|
||||||
target: "http://localhost:6012"
|
target: "http://localhost:6012"
|
||||||
@@ -178,7 +179,7 @@ jobs:
|
|||||||
cmd_options: "-I"
|
cmd_options: "-I"
|
||||||
|
|
||||||
a11y-scan:
|
a11y-scan:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
|
|||||||
6
.github/workflows/daily_checks.yml
vendored
6
.github/workflows/daily_checks.yml
vendored
@@ -16,7 +16,7 @@ env:
|
|||||||
FLASK_APP: application.py
|
FLASK_APP: application.py
|
||||||
WERKZEUG_DEBUG_PIN: off
|
WERKZEUG_DEBUG_PIN: off
|
||||||
REDIS_ENABLED: 0
|
REDIS_ENABLED: 0
|
||||||
NODE_VERSION: 22.3.0
|
NODE_VERSION: 16.15.1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dependency-audits:
|
dependency-audits:
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
- name: Create requirements.txt
|
- name: Create requirements.txt
|
||||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
- uses: pypa/gh-action-pip-audit@v1.1.0
|
- uses: pypa/gh-action-pip-audit@v1.0.6
|
||||||
with:
|
with:
|
||||||
inputs: requirements.txt
|
inputs: requirements.txt
|
||||||
- name: Run npm audit
|
- name: Run npm audit
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NOTIFY_ENVIRONMENT: scanning
|
NOTIFY_ENVIRONMENT: scanning
|
||||||
- name: Run OWASP Full Scan
|
- name: Run OWASP Full Scan
|
||||||
uses: zaproxy/action-full-scan@v0.12.0
|
uses: zaproxy/action-full-scan@v0.7.0
|
||||||
with:
|
with:
|
||||||
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
|
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
|
||||||
target: 'http://localhost:6012'
|
target: 'http://localhost:6012'
|
||||||
|
|||||||
25
.github/workflows/deploy-demo.yml
vendored
25
.github/workflows/deploy-demo.yml
vendored
@@ -16,21 +16,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
- name: Check for changes to Terraform
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
id: changed-terraform-files
|
||||||
- name: Setup Terraform
|
uses: tj-actions/changed-files@v44
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
with:
|
||||||
terraform_version: "^1.7.5"
|
files: |
|
||||||
terraform_wrapper: false
|
terraform/demo/**
|
||||||
|
terraform/shared/**
|
||||||
|
.github/workflows/deploy-demo.yml
|
||||||
- name: Terraform init
|
- name: Terraform init
|
||||||
|
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||||
working-directory: terraform/demo
|
working-directory: terraform/demo
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||||
run: terraform init
|
run: terraform init
|
||||||
- name: Terraform apply
|
- name: Terraform apply
|
||||||
|
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||||
working-directory: terraform/demo
|
working-directory: terraform/demo
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||||
@@ -89,7 +91,16 @@ jobs:
|
|||||||
--var LOGIN_PEM="$LOGIN_PEM"
|
--var LOGIN_PEM="$LOGIN_PEM"
|
||||||
--strategy rolling
|
--strategy rolling
|
||||||
|
|
||||||
|
- name: Check for changes to egress config
|
||||||
|
id: changed-egress-config
|
||||||
|
uses: tj-actions/changed-files@v44
|
||||||
|
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
|
- name: Deploy egress proxy
|
||||||
|
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||||
uses: ./.github/actions/deploy-proxy
|
uses: ./.github/actions/deploy-proxy
|
||||||
env:
|
env:
|
||||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||||
|
|||||||
25
.github/workflows/deploy-prod.yml
vendored
25
.github/workflows/deploy-prod.yml
vendored
@@ -16,21 +16,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
- name: Check for changes to Terraform
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
id: changed-terraform-files
|
||||||
- name: Setup Terraform
|
uses: tj-actions/changed-files@v44
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
with:
|
||||||
terraform_version: "^1.7.5"
|
files: |
|
||||||
terraform_wrapper: false
|
terraform/production/**
|
||||||
|
terraform/shared/**
|
||||||
|
.github/workflows/deploy-prod.yml
|
||||||
- name: Terraform init
|
- name: Terraform init
|
||||||
|
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||||
working-directory: terraform/production
|
working-directory: terraform/production
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||||
run: terraform init
|
run: terraform init
|
||||||
- name: Terraform apply
|
- name: Terraform apply
|
||||||
|
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||||
working-directory: terraform/production
|
working-directory: terraform/production
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||||
@@ -89,7 +91,16 @@ jobs:
|
|||||||
--var LOGIN_PEM="$LOGIN_PEM"
|
--var LOGIN_PEM="$LOGIN_PEM"
|
||||||
--strategy rolling
|
--strategy rolling
|
||||||
|
|
||||||
|
- name: Check for changes to egress config
|
||||||
|
id: changed-egress-config
|
||||||
|
uses: tj-actions/changed-files@v44
|
||||||
|
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
|
- name: Deploy egress proxy
|
||||||
|
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||||
uses: ./.github/actions/deploy-proxy
|
uses: ./.github/actions/deploy-proxy
|
||||||
env:
|
env:
|
||||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||||
|
|||||||
25
.github/workflows/deploy.yml
vendored
25
.github/workflows/deploy.yml
vendored
@@ -21,21 +21,23 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
- name: Check for changes to Terraform
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
id: changed-terraform-files
|
||||||
- name: Setup Terraform
|
uses: tj-actions/changed-files@v44
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
with:
|
||||||
terraform_version: "^1.7.5"
|
files: |
|
||||||
terraform_wrapper: false
|
terraform/staging/**
|
||||||
|
terraform/shared/**
|
||||||
|
.github/workflows/deploy.yml
|
||||||
- name: Terraform init
|
- name: Terraform init
|
||||||
|
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||||
working-directory: terraform/staging
|
working-directory: terraform/staging
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||||
run: terraform init
|
run: terraform init
|
||||||
- name: Terraform apply
|
- name: Terraform apply
|
||||||
|
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||||
working-directory: terraform/staging
|
working-directory: terraform/staging
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||||
@@ -96,7 +98,16 @@ jobs:
|
|||||||
--strategy rolling
|
--strategy rolling
|
||||||
|
|
||||||
|
|
||||||
|
- name: Check for changes to egress config
|
||||||
|
id: changed-egress-config
|
||||||
|
uses: tj-actions/changed-files@v44
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
deploy-config/egress_proxy/notify-admin-staging.*.acl
|
||||||
|
.github/actions/deploy-proxy/action.yml
|
||||||
|
.github/workflows/deploy.yml
|
||||||
- name: Deploy egress proxy
|
- name: Deploy egress proxy
|
||||||
|
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||||
uses: ./.github/actions/deploy-proxy
|
uses: ./.github/actions/deploy-proxy
|
||||||
env:
|
env:
|
||||||
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||||
|
|||||||
24
.github/workflows/drift.yml
vendored
24
.github/workflows/drift.yml
vendored
@@ -15,14 +15,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
|
||||||
- name: Setup Terraform
|
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
|
||||||
terraform_version: "^1.7.5"
|
|
||||||
terraform_wrapper: false
|
|
||||||
|
|
||||||
- name: Check for drift
|
- name: Check for drift
|
||||||
uses: dflook/terraform-check@v1
|
uses: dflook/terraform-check@v1
|
||||||
env:
|
env:
|
||||||
@@ -43,14 +35,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: 'production'
|
ref: 'production'
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
|
||||||
- name: Setup Terraform
|
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
|
||||||
terraform_version: "^1.7.5"
|
|
||||||
terraform_wrapper: false
|
|
||||||
|
|
||||||
- name: Check for drift
|
- name: Check for drift
|
||||||
uses: dflook/terraform-check@v1
|
uses: dflook/terraform-check@v1
|
||||||
env:
|
env:
|
||||||
@@ -71,14 +55,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: 'production'
|
ref: 'production'
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
|
||||||
- name: Setup Terraform
|
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
|
||||||
terraform_version: "^1.7.5"
|
|
||||||
terraform_wrapper: false
|
|
||||||
|
|
||||||
- name: Check for drift
|
- name: Check for drift
|
||||||
uses: dflook/terraform-check@v1
|
uses: dflook/terraform-check@v1
|
||||||
env:
|
env:
|
||||||
|
|||||||
10
.github/workflows/terraform-demo.yml
vendored
10
.github/workflows/terraform-demo.yml
vendored
@@ -18,14 +18,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
|
||||||
- name: Setup Terraform
|
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
|
||||||
terraform_version: "^1.7.5"
|
|
||||||
terraform_wrapper: false
|
|
||||||
|
|
||||||
- name: Terraform format
|
- name: Terraform format
|
||||||
id: format
|
id: format
|
||||||
run: terraform fmt -check
|
run: terraform fmt -check
|
||||||
@@ -59,7 +51,7 @@ jobs:
|
|||||||
|
|
||||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||||
- name: Update PR
|
- name: Update PR
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
# we would like to update the PR even when a prior step failed
|
# we would like to update the PR even when a prior step failed
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
.github/workflows/terraform-production.yml
vendored
10
.github/workflows/terraform-production.yml
vendored
@@ -18,14 +18,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
|
||||||
- name: Setup Terraform
|
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
|
||||||
terraform_version: "^1.7.5"
|
|
||||||
terraform_wrapper: false
|
|
||||||
|
|
||||||
- name: Terraform format
|
- name: Terraform format
|
||||||
id: format
|
id: format
|
||||||
run: terraform fmt -check
|
run: terraform fmt -check
|
||||||
@@ -59,7 +51,7 @@ jobs:
|
|||||||
|
|
||||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||||
- name: Update PR
|
- name: Update PR
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
# we would like to update the PR even when a prior step failed
|
# we would like to update the PR even when a prior step failed
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
.github/workflows/terraform-staging.yml
vendored
10
.github/workflows/terraform-staging.yml
vendored
@@ -18,14 +18,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Looks like we need to install Terraform ourselves now!
|
|
||||||
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
|
|
||||||
- name: Setup Terraform
|
|
||||||
uses: hashicorp/setup-terraform@v3
|
|
||||||
with:
|
|
||||||
terraform_version: "^1.7.5"
|
|
||||||
terraform_wrapper: false
|
|
||||||
|
|
||||||
- name: Terraform format
|
- name: Terraform format
|
||||||
id: format
|
id: format
|
||||||
run: terraform fmt -check
|
run: terraform fmt -check
|
||||||
@@ -59,7 +51,7 @@ jobs:
|
|||||||
|
|
||||||
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
|
||||||
- name: Update PR
|
- name: Update PR
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
# we would like to update the PR even when a prior step failed
|
# we would like to update the PR even when a prior step failed
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,10 +15,6 @@
|
|||||||
|
|
||||||
## Non user files allowed to be commited
|
## Non user files allowed to be commited
|
||||||
!app/assets/pdf/tcpa_overview.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
|
|
||||||
!app/assets/pdf/studio-research-snapshot-2022-07-external.pdf
|
|
||||||
!app/assets/pdf/TCPA-Overview.pdf
|
|
||||||
!tests/test_pdf_files/no_eof_marker.pdf
|
!tests/test_pdf_files/no_eof_marker.pdf
|
||||||
!tests/test_pdf_files/multi_page_pdf.pdf
|
!tests/test_pdf_files/multi_page_pdf.pdf
|
||||||
!tests/test_pdf_files/big.pdf
|
!tests/test_pdf_files/big.pdf
|
||||||
|
|||||||
5
.pa11yci
5
.pa11yci
@@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"standard": "WCAG2AA",
|
"standard": "WCAG2AA",
|
||||||
"runners": ["htmlcs"],
|
"runners": ["htmlcs"],
|
||||||
"chromeLaunchConfig": {
|
|
||||||
"executablePath": "/usr/bin/google-chrome"
|
|
||||||
},
|
|
||||||
"concurrency": 1,
|
"concurrency": 1,
|
||||||
"hideElements": [
|
"hideElements": [
|
||||||
"nav > ol a",
|
"nav > ol a",
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -62,13 +62,6 @@ py-lint: ## Run python linting scanners and black
|
|||||||
poetry run flake8 .
|
poetry run flake8 .
|
||||||
poetry run isort --check-only ./app ./tests
|
poetry run isort --check-only ./app ./tests
|
||||||
|
|
||||||
.PHONY: tada
|
|
||||||
tada: ## Run python linting scanners and black
|
|
||||||
poetry run isort ./app ./tests
|
|
||||||
poetry run black .
|
|
||||||
poetry run flake8 .
|
|
||||||
|
|
||||||
|
|
||||||
.PHONY: avg-complexity
|
.PHONY: avg-complexity
|
||||||
avg-complexity:
|
avg-complexity:
|
||||||
echo "*** Shows average complexity in radon of all code ***"
|
echo "*** Shows average complexity in radon of all code ***"
|
||||||
@@ -162,8 +155,3 @@ upload-static:
|
|||||||
# @cf map-route notify-admin ${DNS_NAME} --hostname www
|
# @cf map-route notify-admin ${DNS_NAME} --hostname www
|
||||||
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
|
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
|
||||||
# @echo "Failwhale is disabled"
|
# @echo "Failwhale is disabled"
|
||||||
|
|
||||||
.PHONY: test-single
|
|
||||||
test-single: export NEW_RELIC_ENVIRONMENT=test
|
|
||||||
test-single: ## Run a single test file
|
|
||||||
poetry run pytest $(TEST_FILE)
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
|
||||||
import secrets
|
import secrets
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
@@ -169,15 +168,14 @@ def _csp(config):
|
|||||||
|
|
||||||
|
|
||||||
def create_app(application):
|
def create_app(application):
|
||||||
# @application.context_processor
|
@application.context_processor
|
||||||
# def inject_feature_flags():
|
def inject_feature_flags():
|
||||||
# this is where feature flags can be easily added as a dictionary within context
|
feature_about_page_enabled = application.config.get(
|
||||||
# feature_about_page_enabled = application.config.get(
|
"FEATURE_ABOUT_PAGE_ENABLED", False
|
||||||
# "FEATURE_ABOUT_PAGE_ENABLED", False
|
)
|
||||||
# )
|
return dict(
|
||||||
# return dict(
|
FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled,
|
||||||
# FEATURE_ABOUT_PAGE_ENABLED=feature_about_page_enabled,
|
)
|
||||||
# )
|
|
||||||
|
|
||||||
@application.context_processor
|
@application.context_processor
|
||||||
def inject_initial_signin_url():
|
def inject_initial_signin_url():
|
||||||
@@ -618,8 +616,6 @@ def setup_event_handlers():
|
|||||||
|
|
||||||
|
|
||||||
def add_template_filters(application):
|
def add_template_filters(application):
|
||||||
application.add_template_filter(slugify)
|
|
||||||
|
|
||||||
for fn in [
|
for fn in [
|
||||||
format_auth_type,
|
format_auth_type,
|
||||||
format_billions,
|
format_billions,
|
||||||
@@ -677,10 +673,3 @@ def init_jinja(application):
|
|||||||
]
|
]
|
||||||
jinja_loader = jinja2.FileSystemLoader(template_folders)
|
jinja_loader = jinja2.FileSystemLoader(template_folders)
|
||||||
application.jinja_loader = jinja_loader
|
application.jinja_loader = jinja_loader
|
||||||
|
|
||||||
|
|
||||||
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()))
|
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
(function (window) {
|
(function (window) {
|
||||||
|
|
||||||
if (document.getElementById('activityChartContainer')) {
|
if (document.getElementById('activityChartContainer')) {
|
||||||
let currentType = 'service';
|
|
||||||
const tableContainer = document.getElementById('activityContainer');
|
|
||||||
const currentUserName = tableContainer.getAttribute('data-currentUserName');
|
|
||||||
const currentServiceId = tableContainer.getAttribute('data-currentServiceId');
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
delivered: '#0076d6',
|
delivered: '#0076d6',
|
||||||
failed: '#fa9441',
|
failed: '#fa9441',
|
||||||
pending: '#C7CACE',
|
|
||||||
text: '#666'
|
text: '#666'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,7 +12,7 @@
|
|||||||
const FONT_WEIGHT = 'bold';
|
const FONT_WEIGHT = 'bold';
|
||||||
const MAX_Y = 120;
|
const MAX_Y = 120;
|
||||||
|
|
||||||
const createChart = function(containerId, labels, deliveredData, failedData, pendingData) {
|
const createChart = function(containerId, labels, deliveredData, failedData) {
|
||||||
const container = d3.select(containerId);
|
const container = d3.select(containerId);
|
||||||
container.selectAll('*').remove(); // Clear any existing content
|
container.selectAll('*').remove(); // Clear any existing content
|
||||||
|
|
||||||
@@ -39,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total messages
|
// Calculate total messages
|
||||||
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData) + d3.sum(pendingData);
|
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData);
|
||||||
|
|
||||||
// Create legend only if there are messages
|
// Create legend only if there are messages
|
||||||
const legendContainer = d3.select('.chart-legend');
|
const legendContainer = d3.select('.chart-legend');
|
||||||
@@ -49,8 +45,7 @@
|
|||||||
// Show legend if there are messages
|
// Show legend if there are messages
|
||||||
const legendData = [
|
const legendData = [
|
||||||
{ label: 'Delivered', color: COLORS.delivered },
|
{ label: 'Delivered', color: COLORS.delivered },
|
||||||
{ label: 'Failed', color: COLORS.failed },
|
{ label: 'Failed', color: COLORS.failed }
|
||||||
{ label: 'Pending', color: COLORS.pending }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const legendItem = legendContainer.selectAll('.legend-item')
|
const legendItem = legendContainer.selectAll('.legend-item')
|
||||||
@@ -81,9 +76,8 @@
|
|||||||
.range([0, width])
|
.range([0, width])
|
||||||
.padding(0.1);
|
.padding(0.1);
|
||||||
// Adjust the y-axis domain to add some space above the tallest bar
|
// Adjust the y-axis domain to add some space above the tallest bar
|
||||||
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0) + (pendingData[i] || 0)));
|
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0)));
|
||||||
|
const y = d3.scaleSqrt()
|
||||||
const y = d3.scaleSymlog()
|
|
||||||
.domain([0, maxY + 2]) // Add 2 units of space at the top
|
.domain([0, maxY + 2]) // Add 2 units of space at the top
|
||||||
.nice()
|
.nice()
|
||||||
.range([height, 0]);
|
.range([height, 0]);
|
||||||
@@ -95,7 +89,7 @@
|
|||||||
|
|
||||||
// Generate the y-axis with whole numbers
|
// Generate the y-axis with whole numbers
|
||||||
const yAxis = d3.axisLeft(y)
|
const yAxis = d3.axisLeft(y)
|
||||||
.ticks(Math.min(maxY + 2, 3))
|
.ticks(Math.min(maxY + 2, 10)) // Generate up to 10 ticks based on the data
|
||||||
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
|
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
|
||||||
|
|
||||||
svg.append('g')
|
svg.append('g')
|
||||||
@@ -106,13 +100,12 @@
|
|||||||
const stackData = labels.map((label, i) => ({
|
const stackData = labels.map((label, i) => ({
|
||||||
label: label,
|
label: label,
|
||||||
delivered: deliveredData[i],
|
delivered: deliveredData[i],
|
||||||
failed: failedData[i] || 0,
|
failed: failedData[i] || 0 // Ensure there's a value for failed, even if it's 0
|
||||||
pending: pendingData[i] || 0
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Stack the data
|
// Stack the data
|
||||||
const stack = d3.stack()
|
const stack = d3.stack()
|
||||||
.keys(['delivered', 'failed', 'pending'])
|
.keys(['delivered', 'failed'])
|
||||||
.order(d3.stackOrderNone)
|
.order(d3.stackOrderNone)
|
||||||
.offset(d3.stackOffsetNone);
|
.offset(d3.stackOffsetNone);
|
||||||
|
|
||||||
@@ -120,8 +113,8 @@
|
|||||||
|
|
||||||
// Color scale
|
// Color scale
|
||||||
const color = d3.scaleOrdinal()
|
const color = d3.scaleOrdinal()
|
||||||
.domain(['delivered', 'failed', 'pending'])
|
.domain(['delivered', 'failed'])
|
||||||
.range([COLORS.delivered, COLORS.failed, COLORS.pending]);
|
.range([COLORS.delivered, COLORS.failed]);
|
||||||
|
|
||||||
// Create bars with animation
|
// Create bars with animation
|
||||||
const barGroups = svg.selectAll('.bar-group')
|
const barGroups = svg.selectAll('.bar-group')
|
||||||
@@ -130,12 +123,11 @@
|
|||||||
.append('g')
|
.append('g')
|
||||||
.attr('class', 'bar-group')
|
.attr('class', 'bar-group')
|
||||||
.attr('fill', d => color(d.key));
|
.attr('fill', d => color(d.key));
|
||||||
const minBarHeight = 5;
|
|
||||||
barGroups.selectAll('rect')
|
barGroups.selectAll('rect')
|
||||||
.data(d => d)
|
.data(d => d)
|
||||||
.enter()
|
.enter()
|
||||||
.append('rect')
|
.append('rect')
|
||||||
.filter(d => d[1] - d[0] > 0)
|
|
||||||
.attr('x', d => x(d.data.label))
|
.attr('x', d => x(d.data.label))
|
||||||
.attr('y', height)
|
.attr('y', height)
|
||||||
.attr('height', 0)
|
.attr('height', 0)
|
||||||
@@ -156,13 +148,11 @@
|
|||||||
.transition()
|
.transition()
|
||||||
.duration(1000)
|
.duration(1000)
|
||||||
.attr('y', d => y(d[1]))
|
.attr('y', d => y(d[1]))
|
||||||
.attr('height', d => {
|
.attr('height', d => y(d[0]) - y(d[1]));
|
||||||
const calculatedHeight = y(d[0]) - y(d[1]);
|
};
|
||||||
return calculatedHeight < minBarHeight ? minBarHeight : calculatedHeight;
|
|
||||||
}); };
|
|
||||||
|
|
||||||
// Function to create an accessible table
|
// Function to create an accessible table
|
||||||
const createTable = function(tableId, chartType, labels, deliveredData, failedData, pendingData) {
|
const createTable = function(tableId, chartType, labels, deliveredData, failedData) {
|
||||||
const table = document.getElementById(tableId);
|
const table = document.getElementById(tableId);
|
||||||
table.innerHTML = ""; // Clear previous data
|
table.innerHTML = ""; // Clear previous data
|
||||||
|
|
||||||
@@ -174,7 +164,7 @@
|
|||||||
|
|
||||||
// Create table header
|
// Create table header
|
||||||
const headerRow = document.createElement('tr');
|
const headerRow = document.createElement('tr');
|
||||||
const headers = ['Day', 'Delivered', 'Failed', 'Pending'];
|
const headers = ['Day', 'Delivered', 'Failed'];
|
||||||
headers.forEach(headerText => {
|
headers.forEach(headerText => {
|
||||||
const th = document.createElement('th');
|
const th = document.createElement('th');
|
||||||
th.textContent = headerText;
|
th.textContent = headerText;
|
||||||
@@ -197,10 +187,6 @@
|
|||||||
cellFailed.textContent = failedData[index];
|
cellFailed.textContent = failedData[index];
|
||||||
row.appendChild(cellFailed);
|
row.appendChild(cellFailed);
|
||||||
|
|
||||||
const cellPending = document.createElement('td');
|
|
||||||
cellPending.textContent = pendingData[index];
|
|
||||||
row.appendChild(cellPending);
|
|
||||||
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,19 +196,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = function(type) {
|
const fetchData = function(type) {
|
||||||
|
|
||||||
var ctx = document.getElementById('weeklyChart');
|
var ctx = document.getElementById('weeklyChart');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
var url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
|
||||||
|
|
||||||
var url = type === 'service'
|
|
||||||
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
|
|
||||||
: `/services/${currentServiceId}/daily-stats-by-user.json?timezone=${encodeURIComponent(userTimezone)}`;
|
|
||||||
|
|
||||||
|
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -234,7 +213,7 @@
|
|||||||
labels = [];
|
labels = [];
|
||||||
deliveredData = [];
|
deliveredData = [];
|
||||||
failedData = [];
|
failedData = [];
|
||||||
pendingData = [];
|
|
||||||
let totalMessages = 0;
|
let totalMessages = 0;
|
||||||
|
|
||||||
for (var dateString in data) {
|
for (var dateString in data) {
|
||||||
@@ -245,8 +224,9 @@
|
|||||||
labels.push(formattedDate);
|
labels.push(formattedDate);
|
||||||
deliveredData.push(data[dateString].sms.delivered);
|
deliveredData.push(data[dateString].sms.delivered);
|
||||||
failedData.push(data[dateString].sms.failure);
|
failedData.push(data[dateString].sms.failure);
|
||||||
pendingData.push(data[dateString].sms.pending || 0);
|
|
||||||
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure + data[dateString].sms.pending;
|
// Calculate the total number of messages
|
||||||
|
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,18 +252,17 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If there are messages, create the chart and table
|
// If there are messages, create the chart and table
|
||||||
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
|
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching daily stats:', error));
|
||||||
|
};
|
||||||
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error fetching daily stats:', error));
|
|
||||||
};
|
|
||||||
setInterval(() => fetchData(currentType), 25000);
|
|
||||||
const handleDropdownChange = function(event) {
|
const handleDropdownChange = function(event) {
|
||||||
const selectedValue = event.target.value;
|
const selectedValue = event.target.value;
|
||||||
currentType = selectedValue;
|
|
||||||
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
|
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
|
||||||
const selectElement = document.getElementById('options');
|
const selectElement = document.getElementById('options');
|
||||||
const selectedText = selectElement.options[selectElement.selectedIndex].text;
|
const selectedText = selectElement.options[selectElement.selectedIndex].text;
|
||||||
@@ -291,67 +270,36 @@
|
|||||||
subTitle.textContent = `${selectedText} - last 7 days`;
|
subTitle.textContent = `${selectedText} - last 7 days`;
|
||||||
fetchData(selectedValue);
|
fetchData(selectedValue);
|
||||||
|
|
||||||
|
// Update ARIA live region
|
||||||
const liveRegion = document.getElementById('aria-live-account');
|
const liveRegion = document.getElementById('aria-live-account');
|
||||||
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
|
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
|
||||||
|
|
||||||
const tableHeading = document.querySelector('#tableActivity h2');
|
// Switch tables based on dropdown selection
|
||||||
const senderColumns = document.querySelectorAll('.sender-column');
|
const selectedTable = selectedValue === "individual" ? "table1" : "table2";
|
||||||
const allRows = document.querySelectorAll('#activity-table tbody tr');
|
const tables = document.querySelectorAll('.table-overflow-x-auto');
|
||||||
const caption = document.querySelector('#activity-table caption');
|
tables.forEach(function(table) {
|
||||||
|
table.classList.add('hidden'); // Hide all tables by adding the hidden class
|
||||||
if (selectedValue === 'individual') {
|
table.classList.remove('visible'); // Ensure they are not visible
|
||||||
|
});
|
||||||
tableHeading.textContent = 'My activity';
|
const tableToShow = document.getElementById(selectedTable);
|
||||||
caption.textContent = `Table showing the sent jobs for ${currentUserName}`;
|
tableToShow.classList.remove('hidden'); // Remove hidden class
|
||||||
|
tableToShow.classList.add('visible'); // Add visible class
|
||||||
senderColumns.forEach(col => {
|
|
||||||
col.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
allRows.forEach(row => row.style.display = 'none');
|
|
||||||
|
|
||||||
const userRows = Array.from(allRows).filter(row => {
|
|
||||||
const senderCell = row.querySelector('.sender-column');
|
|
||||||
const rowSender = senderCell ? senderCell.textContent.trim() : '';
|
|
||||||
return rowSender === currentUserName;
|
|
||||||
});
|
|
||||||
|
|
||||||
userRows.slice(0, 5).forEach(row => {
|
|
||||||
row.style.display = '';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
|
|
||||||
tableHeading.textContent = 'Service activity';
|
|
||||||
caption.textContent = `Table showing the sent jobs for service`;
|
|
||||||
|
|
||||||
senderColumns.forEach(col => {
|
|
||||||
col.style.display = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
allRows.forEach((row, index) => {
|
|
||||||
row.style.display = (index < 5) ? '' : 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize activityChart chart and table with service data by default
|
// Initialize activityChart chart and table with service data by default
|
||||||
fetchData(currentType);
|
fetchData('service');
|
||||||
|
|
||||||
const allRows = Array.from(document.querySelectorAll('#activity-table tbody tr'));
|
|
||||||
allRows.forEach((row, index) => {
|
|
||||||
row.style.display = (index < 5) ? '' : 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Add event listener to the dropdown
|
||||||
const dropdown = document.getElementById('options');
|
const dropdown = document.getElementById('options');
|
||||||
dropdown.addEventListener('change', handleDropdownChange);
|
dropdown.addEventListener('change', handleDropdownChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resize chart on window resize
|
// Resize chart on window resize
|
||||||
window.addEventListener('resize', function() {
|
window.addEventListener('resize', function() {
|
||||||
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0 && pendingData.length > 0) {
|
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0) {
|
||||||
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
|
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
|
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,23 +13,16 @@
|
|||||||
} else {
|
} else {
|
||||||
|
|
||||||
$submitButton.data('clicked', 'true');
|
$submitButton.data('clicked', 'true');
|
||||||
|
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||||
|
|
||||||
if ($submitButton.is('[name="Send"], [name="Schedule"]')) {
|
|
||||||
$submitButton.prop('disabled', true);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
renableSubmitButton($submitButton);
|
|
||||||
}, 10000);
|
|
||||||
} else {
|
|
||||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let renableSubmitButton = $submitButton => () => {
|
let renableSubmitButton = $submitButton => () => {
|
||||||
|
|
||||||
$submitButton.data('clicked', '');
|
$submitButton.data('clicked', '');
|
||||||
$submitButton.prop('disabled', false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$('form').on('submit', disableSubmitButtons);
|
$('form').on('submit', disableSubmitButtons);
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
var chartTitle = document.getElementById('chartTitle').textContent;
|
var chartTitle = document.getElementById('chartTitle').textContent;
|
||||||
|
|
||||||
// Access data attributes from the HTML
|
// Access data attributes from the HTML
|
||||||
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
|
var sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
|
||||||
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
|
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
|
||||||
var totalMessages = messagesSent + messagesRemaining;
|
var totalMessages = sms_sent + sms_remaining_messages;
|
||||||
|
|
||||||
// Update the message below the chart
|
// Update the message below the chart
|
||||||
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
|
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
|
||||||
|
|
||||||
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
|
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
|
||||||
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
|
var minSentPercentage = (sms_sent === 0) ? 0 : 0.02;
|
||||||
var minSentValue = totalMessages * minSentPercentage;
|
var minSentValue = totalMessages * minSentPercentage;
|
||||||
var displaySent = Math.max(messagesSent, minSentValue);
|
var displaySent = Math.max(sms_sent, minSentValue);
|
||||||
var displayRemaining = totalMessages - displaySent;
|
var displayRemaining = totalMessages - displaySent;
|
||||||
|
|
||||||
var svg = d3.select("#totalMessageChart");
|
var svg = d3.select("#totalMessageChart");
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
.attr("width", 0) // Start with width 0 for animation
|
.attr("width", 0) // Start with width 0 for animation
|
||||||
.on('mouseover', function(event) {
|
.on('mouseover', function(event) {
|
||||||
tooltip.style('display', 'block')
|
tooltip.style('display', 'block')
|
||||||
.html(`Messages Sent: ${messagesSent.toLocaleString()}`);
|
.html(`Messages Sent: ${sms_sent.toLocaleString()}`);
|
||||||
})
|
})
|
||||||
.on('mousemove', function(event) {
|
.on('mousemove', function(event) {
|
||||||
tooltip.style('left', `${event.pageX + 10}px`)
|
tooltip.style('left', `${event.pageX + 10}px`)
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
.attr("width", 0) // Start with width 0 for animation
|
.attr("width", 0) // Start with width 0 for animation
|
||||||
.on('mouseover', function(event) {
|
.on('mouseover', function(event) {
|
||||||
tooltip.style('display', 'block')
|
tooltip.style('display', 'block')
|
||||||
.html(`Remaining: ${messagesRemaining.toLocaleString()}`);
|
.html(`Remaining: ${sms_remaining_messages.toLocaleString()}`);
|
||||||
})
|
})
|
||||||
.on('mousemove', function(event) {
|
.on('mousemove', function(event) {
|
||||||
tooltip.style('left', `${event.pageX + 10}px`)
|
tooltip.style('left', `${event.pageX + 10}px`)
|
||||||
@@ -115,9 +115,9 @@
|
|||||||
var tbodyRow = document.createElement('tr');
|
var tbodyRow = document.createElement('tr');
|
||||||
|
|
||||||
var tdMessagesSent = document.createElement('td');
|
var tdMessagesSent = document.createElement('td');
|
||||||
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
|
tdMessagesSent.textContent = sms_sent.toLocaleString(); // Value for Messages Sent
|
||||||
var tdRemaining = document.createElement('td');
|
var tdRemaining = document.createElement('td');
|
||||||
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
|
tdRemaining.textContent = sms_remaining_messages.toLocaleString(); // Value for Remaining
|
||||||
|
|
||||||
tbodyRow.appendChild(tdMessagesSent);
|
tbodyRow.appendChild(tdMessagesSent);
|
||||||
tbodyRow.appendChild(tdRemaining);
|
tbodyRow.appendChild(tdRemaining);
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
function showError(input, errorElement, message) {
|
|
||||||
errorElement.textContent = ""; // Clear existing message
|
|
||||||
errorElement.style.display = "block";
|
|
||||||
|
|
||||||
// Small delay to ensure screen readers pick up the change
|
|
||||||
setTimeout(() => {
|
|
||||||
errorElement.textContent = message;
|
|
||||||
}, 10);
|
|
||||||
|
|
||||||
if (input.type !== "radio" && input.type !== "checkbox") {
|
|
||||||
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.removeAttribute("aria-describedby");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFieldLabel(input) {
|
|
||||||
const label = document.querySelector(`label[for="${input.id}"]`);
|
|
||||||
return label ? label.textContent.trim() : "This field";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach validation logic to forms
|
|
||||||
function attachValidation() {
|
|
||||||
const forms = document.querySelectorAll('form[data-force-focus="True"]');
|
|
||||||
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) => {
|
|
||||||
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
|
|
||||||
let errorElement = document.getElementById(errorId);
|
|
||||||
|
|
||||||
if (!errorElement) {
|
|
||||||
errorElement = document.createElement("span");
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.type === "radio") {
|
|
||||||
if (validatedRadioNames.has(input.name)) return;
|
|
||||||
validatedRadioNames.add(input.name);
|
|
||||||
const radioGroup = form.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.`);
|
|
||||||
isValid = false;
|
|
||||||
if (!firstInvalidInput) {
|
|
||||||
firstInvalidInput = input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (input.value.trim() === "") {
|
|
||||||
showError(input, errorElement, `Error: ${getFieldLabel(input)} is required.`);
|
|
||||||
isValid = false;
|
|
||||||
if (!firstInvalidInput) {
|
|
||||||
firstInvalidInput = input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (firstInvalidInput) firstInvalidInput.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
inputs.forEach((input) => {
|
|
||||||
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() !== "") {
|
|
||||||
hideError(input, errorElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (input.type === "radio") {
|
|
||||||
input.addEventListener("change", function () {
|
|
||||||
const errorElement = document.getElementById(`${input.name}-error`);
|
|
||||||
if (errorElement) {
|
|
||||||
hideError(input, errorElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Automatically attach validation only in the browser
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
document.addEventListener("DOMContentLoaded", attachValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 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 = { showError, hideError, getFieldLabel, attachValidation };
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -60,6 +60,17 @@ $failed: color('gray-cool-20');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-tooltip {
|
||||||
|
line-height: 1;
|
||||||
|
.usa-tooltip__body {
|
||||||
|
width: units(mobile);
|
||||||
|
font-size: size("body", 2);
|
||||||
|
height: auto;
|
||||||
|
white-space: wrap;
|
||||||
|
line-height: units(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 464px;
|
max-width: 464px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: units(2);
|
padding: units(1);
|
||||||
background: color('gray-cool-10');
|
background: color('gray-cool-10');
|
||||||
border: 1px solid color('gray-cool-10');
|
border: 1px solid color('gray-cool-10');
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|||||||
@@ -139,6 +139,11 @@ td.table-empty-message {
|
|||||||
font-family: family('sans');
|
font-family: family('sans');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-dark-background .pill-item__label {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pill-item.usa-link {
|
.pill-item.usa-link {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
@@ -193,6 +198,9 @@ td.table-empty-message {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// border: 1px solid color('gray-cool-10');
|
||||||
|
// padding: units(2);
|
||||||
|
|
||||||
.tick-cross-list-permissions {
|
.tick-cross-list-permissions {
|
||||||
margin: units(1) 0;
|
margin: units(1) 0;
|
||||||
padding-left: units(2);
|
padding-left: units(2);
|
||||||
@@ -249,11 +257,6 @@ td.table-empty-message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-checkbox.template-list-item.template-list-item-with-checkbox.template-list-item-without-ancestors {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usa-checkbox__label-description {
|
.usa-checkbox__label-description {
|
||||||
margin: units(2px) 0 units(1) units(4);
|
margin: units(2px) 0 units(1) units(4);
|
||||||
}
|
}
|
||||||
@@ -285,8 +288,7 @@ td.table-empty-message {
|
|||||||
@include u-width('mobile-lg');
|
@include u-width('mobile-lg');
|
||||||
margin-top: units(2);
|
margin-top: units(2);
|
||||||
}
|
}
|
||||||
input#search-by-name {
|
input#search {
|
||||||
margin-top: units(1);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid color('gray-60');
|
border: 1px solid color('gray-60');
|
||||||
}
|
}
|
||||||
@@ -550,24 +552,6 @@ td.table-empty-message {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-tooltip {
|
|
||||||
&__information {
|
|
||||||
background: color('ink');
|
|
||||||
margin-top: 4px;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
height: units(3);
|
|
||||||
width: units(3);
|
|
||||||
border: 1px solid color('ink');
|
|
||||||
}
|
|
||||||
.usa-tooltip__body {
|
|
||||||
min-width: units('card-lg');
|
|
||||||
max-width: units('mobile');
|
|
||||||
white-space: normal;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -591,31 +575,19 @@ td.table-empty-message {
|
|||||||
.big-number-smallest {
|
.big-number-smallest {
|
||||||
font-size: units(3);
|
font-size: units(3);
|
||||||
}
|
}
|
||||||
.pill-item__label {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&:not(.pill-item--selected):hover {
|
&:not(.pill-item--selected):hover {
|
||||||
background: color('blue-warm-70v');
|
background: color('blue-warm-70v');
|
||||||
|
|
||||||
.pill-item__label {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.pill-item--selected {
|
&.pill-item--selected:hover {
|
||||||
.pill-item__label {
|
color: color('blue-60v');
|
||||||
color: color('blue-60v');
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
.pill-item__label {
|
|
||||||
color: color('blue-60v');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Etc
|
||||||
|
|
||||||
.email-brand,
|
.email-brand,
|
||||||
.browse-list {
|
.browse-list {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -1052,11 +1024,3 @@ nav.nav {
|
|||||||
font-size: units(3);
|
font-size: units(3);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control-error {
|
|
||||||
border: 4px solid #b10e1e
|
|
||||||
}
|
|
||||||
|
|
||||||
.usa-site-alert .usa-alert .usa-alert__body {
|
|
||||||
max-width: 75rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ class Config(object):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FEATURE_ABOUT_PAGE_ENABLED = getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true"
|
||||||
|
|
||||||
|
|
||||||
def _s3_credentials_from_env(bucket_prefix):
|
def _s3_credentials_from_env(bucket_prefix):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Explore Notify, add team members, and practice [sending messages to teammates](/using-notify/trial-mode).
|
Explore Notify, add team members, and practice [sending messages to teammates](/using-notify/trial-mode).
|
||||||
|
|
||||||
2. ## Personalize content
|
2. ## Personalize content
|
||||||
Learn how to [personalize messages](/using-notify/how-to) to increase response.
|
Learn how to [personalize messages](/using-notify/guidance) to increase response.
|
||||||
|
|
||||||
3. ## Check delivery status
|
3. ## Check delivery status
|
||||||
[Analyze the delivery](/using-notify/delivery-status) of your messages and download reports
|
[Analyze the delivery](/using-notify/delivery-status) of your messages and download reports
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import csv
|
||||||
|
from io import StringIO
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
@@ -28,6 +30,16 @@ class CsvFileValidator:
|
|||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def __call__(self, form, field):
|
def __call__(self, form, field):
|
||||||
|
|
||||||
|
data = Spreadsheet.from_file_form(form).as_dict
|
||||||
|
data = data["data"]
|
||||||
|
if data is not None and data != "":
|
||||||
|
csv_file = StringIO(data)
|
||||||
|
reader = csv.reader(csv_file)
|
||||||
|
first_line = next(reader, None)
|
||||||
|
if first_line is None or all(cell.strip() == "" for cell in first_line):
|
||||||
|
raise ValidationError(f"No headers on row 1 in {field.data.filename}")
|
||||||
|
|
||||||
if not Spreadsheet.can_handle(field.data.filename):
|
if not Spreadsheet.can_handle(field.data.filename):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"{} is not a spreadsheet that Notify can read".format(
|
"{} is not a spreadsheet that Notify can read".format(
|
||||||
@@ -61,7 +73,7 @@ class ValidEmail:
|
|||||||
|
|
||||||
|
|
||||||
class NoCommasInPlaceHolders:
|
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
|
self.message = message
|
||||||
|
|
||||||
def __call__(self, form, field):
|
def __call__(self, form, field):
|
||||||
@@ -108,17 +120,33 @@ class OnlySMSCharacters:
|
|||||||
)
|
)
|
||||||
if non_sms_characters:
|
if non_sms_characters:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"Please remove the unaccepted character {} in your message, then save again".format(
|
"You cannot use {} in {}. {} will not show up properly on everyone’s phones.".format(
|
||||||
formatted_list(
|
formatted_list(
|
||||||
non_sms_characters,
|
non_sms_characters,
|
||||||
conjunction="and",
|
conjunction="or",
|
||||||
before_each="",
|
before_each="",
|
||||||
after_each="",
|
after_each="",
|
||||||
),
|
),
|
||||||
|
{
|
||||||
|
"sms": "text messages",
|
||||||
|
}.get(self._template_type),
|
||||||
|
("It" if len(non_sms_characters) == 1 else "They"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# class NoPlaceholders:
|
||||||
|
|
||||||
|
# def __init__(self, message=None):
|
||||||
|
# self.message = message or (
|
||||||
|
# 'You can’t use ((double brackets)) to personalize this message'
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def __call__(self, form, field):
|
||||||
|
# if Field(field.data).placeholders:
|
||||||
|
# raise ValidationError(self.message)
|
||||||
|
|
||||||
|
|
||||||
class LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly:
|
class LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly:
|
||||||
regex = re.compile(r"^[a-zA-Z0-9\s\._']+$")
|
regex = re.compile(r"^[a-zA-Z0-9\s\._']+$")
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
import calendar
|
import calendar
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
from flask import abort, jsonify, render_template, request, session, url_for
|
from flask import Response, abort, jsonify, render_template, request, session, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
from app import (
|
from app import (
|
||||||
billing_api_client,
|
billing_api_client,
|
||||||
|
current_service,
|
||||||
job_api_client,
|
job_api_client,
|
||||||
service_api_client,
|
service_api_client,
|
||||||
template_statistics_client,
|
template_statistics_client,
|
||||||
)
|
)
|
||||||
|
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
||||||
from app.main import main
|
from app.main import main
|
||||||
from app.main.views.user_profile import set_timezone
|
from app.main.views.user_profile import set_timezone
|
||||||
from app.statistics_utils import get_formatted_percentage
|
from app.statistics_utils import get_formatted_percentage
|
||||||
from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, REQUESTED_STATUSES
|
from app.utils import (
|
||||||
|
DELIVERED_STATUSES,
|
||||||
|
FAILURE_STATUSES,
|
||||||
|
REQUESTED_STATUSES,
|
||||||
|
service_has_permission,
|
||||||
|
)
|
||||||
|
from app.utils.csv import Spreadsheet
|
||||||
|
from app.utils.pagination import generate_next_dict, generate_previous_dict
|
||||||
from app.utils.time import get_current_financial_year
|
from app.utils.time import get_current_financial_year
|
||||||
from app.utils.user import user_has_permissions
|
from app.utils.user import user_has_permissions
|
||||||
|
from notifications_utils.recipients import format_phone_number_human_readable
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/dashboard")
|
@main.route("/services/<uuid:service_id>/dashboard")
|
||||||
@@ -39,111 +48,87 @@ def service_dashboard(service_id):
|
|||||||
if not current_user.has_permissions("view_activity"):
|
if not current_user.has_permissions("view_activity"):
|
||||||
return redirect(url_for("main.choose_template", service_id=service_id))
|
return redirect(url_for("main.choose_template", service_id=service_id))
|
||||||
|
|
||||||
|
yearly_usage = billing_api_client.get_annual_usage_for_service(
|
||||||
|
service_id,
|
||||||
|
get_current_financial_year(),
|
||||||
|
)
|
||||||
|
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||||
|
current_service.id,
|
||||||
|
)
|
||||||
|
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
|
||||||
|
sms_sent = usage_data["sms_sent"]
|
||||||
|
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
|
||||||
|
|
||||||
job_response = job_api_client.get_jobs(service_id)["data"]
|
job_response = job_api_client.get_jobs(service_id)["data"]
|
||||||
service_data_retention_days = 7
|
service_data_retention_days = 7
|
||||||
|
|
||||||
active_jobs = [job for job in job_response if job["job_status"] != "cancelled"]
|
jobs = [
|
||||||
sorted_jobs = sorted(active_jobs, key=lambda job: job["created_at"], reverse=True)
|
{
|
||||||
job_lists = [
|
"job_id": job["id"],
|
||||||
{**job_dict, "finished_processing": job_is_finished(job_dict)}
|
"time_left": get_time_left(job["created_at"]),
|
||||||
for job_dict in sorted_jobs
|
"download_link": url_for(
|
||||||
|
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
||||||
|
),
|
||||||
|
"view_job_link": url_for(
|
||||||
|
".view_job", service_id=current_service.id, job_id=job["id"]
|
||||||
|
),
|
||||||
|
"created_at": job["created_at"],
|
||||||
|
"processing_finished": job.get("processing_finished"),
|
||||||
|
"processing_started": job.get("processing_started"),
|
||||||
|
"notification_count": job["notification_count"],
|
||||||
|
"created_by": job["created_by"],
|
||||||
|
"template_name": job["template_name"],
|
||||||
|
"original_file_name": job["original_file_name"],
|
||||||
|
}
|
||||||
|
for job in job_response
|
||||||
|
if job["job_status"] != "cancelled"
|
||||||
]
|
]
|
||||||
|
|
||||||
total_messages = service_api_client.get_service_message_ratio(service_id)
|
|
||||||
messages_remaining = total_messages.get("messages_remaining", 0)
|
|
||||||
messages_sent = total_messages.get("messages_sent", 0)
|
|
||||||
all_statistics = template_statistics_client.get_template_statistics_for_service(
|
|
||||||
service_id, limit_days=7
|
|
||||||
)
|
|
||||||
template_statistics = aggregate_template_usage(all_statistics)
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/dashboard/dashboard.html",
|
"views/dashboard/dashboard.html",
|
||||||
jobs=job_lists,
|
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||||
|
partials=get_dashboard_partials(service_id),
|
||||||
|
jobs=jobs,
|
||||||
service_data_retention_days=service_data_retention_days,
|
service_data_retention_days=service_data_retention_days,
|
||||||
messages_remaining=messages_remaining,
|
sms_sent=sms_sent,
|
||||||
messages_sent=messages_sent,
|
sms_allowance_remaining=sms_allowance_remaining,
|
||||||
template_statistics=template_statistics,
|
|
||||||
most_used_template_count=max(
|
|
||||||
[row["count"] for row in template_statistics] or [0]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def job_is_finished(job_dict):
|
@main.route("/daily_stats.json")
|
||||||
done_statuses = DELIVERED_STATUSES + FAILURE_STATUSES + ["cancelled"]
|
def get_daily_stats():
|
||||||
processed_count = sum(
|
service_id = session.get("service_id")
|
||||||
stat["count"]
|
|
||||||
for stat in job_dict["statistics"]
|
|
||||||
if stat["status"] in done_statuses
|
|
||||||
)
|
|
||||||
return job_dict["notification_count"] == processed_count
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/daily-stats.json")
|
|
||||||
@user_has_permissions()
|
|
||||||
def get_daily_stats(service_id):
|
|
||||||
date_range = get_stats_date_range()
|
date_range = get_stats_date_range()
|
||||||
days = date_range["days"]
|
|
||||||
user_timezone = request.args.get("timezone", "UTC")
|
|
||||||
|
|
||||||
stats_utc = service_api_client.get_service_notification_statistics_by_day(
|
stats = service_api_client.get_service_notification_statistics_by_day(
|
||||||
service_id,
|
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||||
start_date=date_range["start_date"],
|
|
||||||
days=days,
|
|
||||||
)
|
)
|
||||||
|
return jsonify(stats)
|
||||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
|
|
||||||
return jsonify(local_stats)
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
|
@main.route("/daily_stats_by_user.json")
|
||||||
tz = ZoneInfo(user_timezone)
|
def get_daily_stats_by_user():
|
||||||
today_local = datetime.now(tz).date()
|
service_id = session.get("service_id")
|
||||||
start_local = today_local - timedelta(days=days - 1)
|
|
||||||
|
|
||||||
# Generate exactly days local dates, each with zeroed stats
|
|
||||||
days_list = [
|
|
||||||
(start_local + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(days)
|
|
||||||
]
|
|
||||||
aggregator = {
|
|
||||||
d: {
|
|
||||||
"sms": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
|
||||||
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
|
|
||||||
}
|
|
||||||
for d in days_list
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert each UTC timestamp to local date and iterate
|
|
||||||
for utc_ts, data in stats_utc.items():
|
|
||||||
utc_dt = datetime.strptime(utc_ts, "%Y-%m-%dT%H:%M:%SZ").replace(
|
|
||||||
tzinfo=ZoneInfo("UTC")
|
|
||||||
)
|
|
||||||
local_day = utc_dt.astimezone(tz).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
if local_day in aggregator:
|
|
||||||
for msg_type in ["sms", "email"]:
|
|
||||||
for status in ["delivered", "failure", "pending", "requested"]:
|
|
||||||
aggregator[local_day][msg_type][status] += data[msg_type][status]
|
|
||||||
|
|
||||||
return aggregator
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/daily-stats-by-user.json")
|
|
||||||
@user_has_permissions()
|
|
||||||
def get_daily_stats_by_user(service_id):
|
|
||||||
date_range = get_stats_date_range()
|
date_range = get_stats_date_range()
|
||||||
days = date_range["days"]
|
user_id = current_user.id
|
||||||
user_timezone = request.args.get("timezone", "UTC")
|
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
||||||
|
|
||||||
stats_utc = service_api_client.get_user_service_notification_statistics_by_day(
|
|
||||||
service_id,
|
service_id,
|
||||||
user_id=current_user.id,
|
user_id,
|
||||||
start_date=date_range["start_date"],
|
start_date=date_range["start_date"],
|
||||||
days=days,
|
days=date_range["days"],
|
||||||
)
|
)
|
||||||
|
return jsonify(stats)
|
||||||
|
|
||||||
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
|
|
||||||
return jsonify(local_stats)
|
@main.route("/services/<uuid:service_id>/dashboard.json")
|
||||||
|
@user_has_permissions("view_activity")
|
||||||
|
def service_dashboard_updates(service_id):
|
||||||
|
return jsonify(**get_dashboard_partials(service_id))
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<uuid:service_id>/template-activity")
|
||||||
|
@user_has_permissions("view_activity")
|
||||||
|
def template_history(service_id):
|
||||||
|
return redirect(url_for("main.template_usage", service_id=service_id), code=301)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/template-usage")
|
@main.route("/services/<uuid:service_id>/template-usage")
|
||||||
@@ -231,6 +216,103 @@ def usage(service_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<uuid:service_id>/monthly")
|
||||||
|
@user_has_permissions("view_activity")
|
||||||
|
def monthly(service_id):
|
||||||
|
year, current_financial_year = requested_and_current_financial_year(request)
|
||||||
|
return render_template(
|
||||||
|
"views/dashboard/monthly.html",
|
||||||
|
months=format_monthly_stats_to_list(
|
||||||
|
service_api_client.get_monthly_notification_stats(service_id, year)["data"]
|
||||||
|
),
|
||||||
|
years=get_tuples_of_financial_years(
|
||||||
|
partial_url=partial(url_for, ".monthly", service_id=service_id),
|
||||||
|
start=current_financial_year - 2,
|
||||||
|
end=current_financial_year,
|
||||||
|
),
|
||||||
|
selected_year=year,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<uuid:service_id>/inbox")
|
||||||
|
@user_has_permissions("view_activity")
|
||||||
|
@service_has_permission("inbound_sms")
|
||||||
|
def inbox(service_id):
|
||||||
|
return render_template(
|
||||||
|
"views/dashboard/inbox.html",
|
||||||
|
partials=get_inbox_partials(service_id),
|
||||||
|
updates_url=url_for(
|
||||||
|
".inbox_updates", service_id=service_id, page=request.args.get("page")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<uuid:service_id>/inbox.json")
|
||||||
|
@user_has_permissions("view_activity")
|
||||||
|
@service_has_permission("inbound_sms")
|
||||||
|
def inbox_updates(service_id):
|
||||||
|
return jsonify(get_inbox_partials(service_id))
|
||||||
|
|
||||||
|
|
||||||
|
@main.route("/services/<uuid:service_id>/inbox.csv")
|
||||||
|
@user_has_permissions("view_activity")
|
||||||
|
def inbox_download(service_id):
|
||||||
|
return Response(
|
||||||
|
Spreadsheet.from_rows(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"Phone number",
|
||||||
|
"Message",
|
||||||
|
"Received",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
[
|
||||||
|
format_phone_number_human_readable(message["user_number"]),
|
||||||
|
message["content"].lstrip(("=+-@")),
|
||||||
|
format_datetime_numeric(message["created_at"]),
|
||||||
|
]
|
||||||
|
for message in service_api_client.get_inbound_sms(service_id)["data"]
|
||||||
|
]
|
||||||
|
).as_csv_data,
|
||||||
|
mimetype="text/csv",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": 'inline; filename="Received text messages {}.csv"'.format(
|
||||||
|
format_date_numeric(datetime.utcnow().isoformat())
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_inbox_partials(service_id):
|
||||||
|
page = int(request.args.get("page", 1))
|
||||||
|
inbound_messages_data = service_api_client.get_most_recent_inbound_sms(
|
||||||
|
service_id, page=page
|
||||||
|
)
|
||||||
|
inbound_messages = inbound_messages_data["data"]
|
||||||
|
if not inbound_messages:
|
||||||
|
inbound_number = current_service.inbound_number
|
||||||
|
else:
|
||||||
|
inbound_number = None
|
||||||
|
|
||||||
|
prev_page = None
|
||||||
|
if page > 1:
|
||||||
|
prev_page = generate_previous_dict("main.inbox", service_id, page)
|
||||||
|
next_page = None
|
||||||
|
if inbound_messages_data["has_next"]:
|
||||||
|
next_page = generate_next_dict("main.inbox", service_id, page)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": render_template(
|
||||||
|
"views/dashboard/_inbox_messages.html",
|
||||||
|
messages=inbound_messages,
|
||||||
|
inbound_number=inbound_number,
|
||||||
|
prev_page=prev_page,
|
||||||
|
next_page=next_page,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def filter_out_cancelled_stats(template_statistics):
|
def filter_out_cancelled_stats(template_statistics):
|
||||||
return [s for s in template_statistics if s["status"] != "cancelled"]
|
return [s for s in template_statistics if s["status"] != "cancelled"]
|
||||||
|
|
||||||
@@ -262,6 +344,71 @@ def aggregate_template_usage(template_statistics, sort_key="count"):
|
|||||||
return sorted(templates, key=lambda x: x[sort_key], reverse=True)
|
return sorted(templates, key=lambda x: x[sort_key], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_notifications_stats(template_statistics):
|
||||||
|
template_statistics = filter_out_cancelled_stats(template_statistics)
|
||||||
|
notifications = {
|
||||||
|
template_type: {status: 0 for status in ("requested", "delivered", "failed")}
|
||||||
|
for template_type in ["sms", "email"]
|
||||||
|
}
|
||||||
|
for stat in template_statistics:
|
||||||
|
notifications[stat["template_type"]]["requested"] += stat["count"]
|
||||||
|
if stat["status"] in DELIVERED_STATUSES:
|
||||||
|
notifications[stat["template_type"]]["delivered"] += stat["count"]
|
||||||
|
elif stat["status"] in FAILURE_STATUSES:
|
||||||
|
notifications[stat["template_type"]]["failed"] += stat["count"]
|
||||||
|
|
||||||
|
return notifications
|
||||||
|
|
||||||
|
|
||||||
|
def get_dashboard_partials(service_id):
|
||||||
|
all_statistics = template_statistics_client.get_template_statistics_for_service(
|
||||||
|
service_id, limit_days=7
|
||||||
|
)
|
||||||
|
template_statistics = aggregate_template_usage(all_statistics)
|
||||||
|
stats = aggregate_notifications_stats(all_statistics)
|
||||||
|
|
||||||
|
dashboard_totals = (get_dashboard_totals(stats),)
|
||||||
|
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||||
|
current_service.id,
|
||||||
|
)
|
||||||
|
# These 2 calls will update the dashboard sms allowance count while in trial mode.
|
||||||
|
billing_api_client.get_monthly_usage_for_service(
|
||||||
|
service_id, get_current_financial_year()
|
||||||
|
)
|
||||||
|
billing_api_client.create_or_update_free_sms_fragment_limit(
|
||||||
|
service_id, free_sms_fragment_limit=free_sms_allowance
|
||||||
|
)
|
||||||
|
|
||||||
|
yearly_usage = billing_api_client.get_annual_usage_for_service(
|
||||||
|
service_id,
|
||||||
|
get_current_financial_year(),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"upcoming": render_template(
|
||||||
|
"views/dashboard/_upcoming.html",
|
||||||
|
),
|
||||||
|
"inbox": render_template(
|
||||||
|
"views/dashboard/_inbox.html",
|
||||||
|
),
|
||||||
|
"totals": render_template(
|
||||||
|
"views/dashboard/_totals.html",
|
||||||
|
service_id=service_id,
|
||||||
|
statistics=dashboard_totals[0],
|
||||||
|
),
|
||||||
|
"template-statistics": render_template(
|
||||||
|
"views/dashboard/template-statistics.html",
|
||||||
|
template_statistics=template_statistics,
|
||||||
|
most_used_template_count=max(
|
||||||
|
[row["count"] for row in template_statistics] or [0]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"usage": render_template(
|
||||||
|
"views/dashboard/_usage.html",
|
||||||
|
**get_annual_usage_breakdown(yearly_usage, free_sms_allowance),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_dashboard_totals(statistics):
|
def get_dashboard_totals(statistics):
|
||||||
|
|
||||||
for msg_type in statistics.values():
|
for msg_type in statistics.values():
|
||||||
|
|||||||
@@ -23,11 +23,10 @@ from app.utils.user import user_is_logged_in
|
|||||||
# Hook to check for feature flags
|
# Hook to check for feature flags
|
||||||
@main.before_request
|
@main.before_request
|
||||||
def check_feature_flags():
|
def check_feature_flags():
|
||||||
# Placeholder for future feature flag checks
|
if request.path.startswith("/about") and not current_app.config.get(
|
||||||
# Example:
|
"FEATURE_ABOUT_PAGE_ENABLED", False
|
||||||
# if request.path.startswith("/some-feature") and not current_app.config.get("FEATURE_SOME_FEATURE_ENABLED", False):
|
):
|
||||||
# abort(404)
|
abort(404)
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@main.route("/test/feature-flags")
|
@main.route("/test/feature-flags")
|
||||||
@@ -218,11 +217,11 @@ def benchmark_performance():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/using-notify/how-to")
|
@main.route("/using-notify/guidance")
|
||||||
@user_is_logged_in
|
@user_is_logged_in
|
||||||
def how_to():
|
def guidance_index():
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/how-to/index.html",
|
"views/guidance/index.html",
|
||||||
navigation_links=using_notify_nav(),
|
navigation_links=using_notify_nav(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -259,37 +258,37 @@ def why_text_messaging():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/notify-service-ending")
|
@main.route("/join-notify")
|
||||||
@user_is_logged_in
|
def join_notify():
|
||||||
def notify_service_ending():
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/notify-service-ending.html",
|
"views/join-notify.html",
|
||||||
|
navigation_links=about_notify_nav(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/using-notify/how-to/create-and-send-messages")
|
@main.route("/using-notify/guidance/create-and-send-messages")
|
||||||
@user_is_logged_in
|
@user_is_logged_in
|
||||||
def create_and_send_messages():
|
def create_and_send_messages():
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/how-to/create-and-send-messages.html",
|
"views/guidance/create-and-send-messages.html",
|
||||||
navigation_links=using_notify_nav(),
|
navigation_links=using_notify_nav(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/using-notify/how-to/edit-and-format-messages")
|
@main.route("/using-notify/guidance/edit-and-format-messages")
|
||||||
@user_is_logged_in
|
@user_is_logged_in
|
||||||
def edit_and_format_messages():
|
def edit_and_format_messages():
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/how-to/edit-and-format-messages.html",
|
"views/guidance/edit-and-format-messages.html",
|
||||||
navigation_links=using_notify_nav(),
|
navigation_links=using_notify_nav(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.route("/using-notify/how-to/send-files-by-email")
|
@main.route("/using-notify/guidance/send-files-by-email")
|
||||||
@user_is_logged_in
|
@user_is_logged_in
|
||||||
def send_files_by_email():
|
def send_files_by_email():
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/how-to/send-files-by-email.html",
|
"views/guidance/send-files-by-email.html",
|
||||||
navigation_links=using_notify_nav(),
|
navigation_links=using_notify_nav(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ def view_job(service_id, job_id):
|
|||||||
|
|
||||||
filter_args = parse_filter_args(request.args)
|
filter_args = parse_filter_args(request.args)
|
||||||
filter_args["status"] = set_status_filters(filter_args)
|
filter_args["status"] = set_status_filters(filter_args)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/jobs/job.html",
|
"views/jobs/job.html",
|
||||||
job=job,
|
job=job,
|
||||||
@@ -401,9 +402,7 @@ def get_job_partials(job):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if request.referrer is not None:
|
if request.referrer is not None:
|
||||||
session["arrived_from_preview_page"] = ("check" in request.referrer) or (
|
session["arrived_from_preview_page"] = "check" in request.referrer
|
||||||
"help=0" in request.referrer
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
session["arrived_from_preview_page"] = False
|
session["arrived_from_preview_page"] = False
|
||||||
|
|
||||||
|
|||||||
@@ -116,70 +116,6 @@ def download_all_users():
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@main.route("/platform-admin/get-redis-report")
|
|
||||||
@user_is_platform_admin
|
|
||||||
def get_redis_report():
|
|
||||||
|
|
||||||
memory_info = redis_client.info("memory")
|
|
||||||
memory_used = memory_info.get("used_memory_human", "N/A")
|
|
||||||
max_memory = memory_info.get("maxmemory_human", "N/A")
|
|
||||||
if max_memory == "0B":
|
|
||||||
max_memory = "No set limit"
|
|
||||||
mem_fragmentation = memory_info.get("mem_fragmentation_ratio", "N/A")
|
|
||||||
frag_quality = "Swapping (bad)"
|
|
||||||
if mem_fragmentation >= 1.0:
|
|
||||||
frag_quality = "Healthy"
|
|
||||||
if mem_fragmentation > 1.5:
|
|
||||||
frag_quality = "Problematic"
|
|
||||||
if mem_fragmentation > 2.0:
|
|
||||||
frag_quality = "Severe fragmentation"
|
|
||||||
|
|
||||||
frag_note = ""
|
|
||||||
if mem_fragmentation > 2.0:
|
|
||||||
frag_note = "Use MEMORY PURGE.\nReplace multiple small keys with hashes.\nAvoid long keys.\nSet max_memory."
|
|
||||||
elif mem_fragmentation < 1.0:
|
|
||||||
frag_note = "Allocate more RAM.\nSet max_memory."
|
|
||||||
|
|
||||||
keys = redis_client.keys("*")
|
|
||||||
key_details = []
|
|
||||||
|
|
||||||
for key in keys:
|
|
||||||
key_type = redis_client.type(key).decode("utf-8")
|
|
||||||
ttl = redis_client.ttl(key)
|
|
||||||
ttl_str = "No Expiry" if ttl == -1 else f"{ttl} seconds"
|
|
||||||
key_details.append(
|
|
||||||
{"Key": key.decode("utf-8"), "Type": key_type, "TTL": ttl_str}
|
|
||||||
)
|
|
||||||
output = StringIO()
|
|
||||||
writer = csv.writer(
|
|
||||||
output,
|
|
||||||
)
|
|
||||||
writer.writerow(["Redis Report"])
|
|
||||||
writer.writerow([])
|
|
||||||
|
|
||||||
writer.writerow(["Memory"])
|
|
||||||
writer.writerow(["", "Memory Used", memory_used])
|
|
||||||
writer.writerow(["", "Max Memory", max_memory])
|
|
||||||
writer.writerow(["", "Memory Fragmentation Ratio", mem_fragmentation])
|
|
||||||
writer.writerow(["", "Memory Fragmentation Quality", frag_quality, frag_note])
|
|
||||||
writer.writerow([])
|
|
||||||
|
|
||||||
writer.writerow(["Keys Overview"])
|
|
||||||
writer.writerow(["", "TTL", "Type", "Key"])
|
|
||||||
for key_detail in key_details:
|
|
||||||
writer.writerow(
|
|
||||||
["", key_detail["TTL"], key_detail["Type"], key_detail["Key"][0:50]]
|
|
||||||
)
|
|
||||||
|
|
||||||
csv_data = output.getvalue()
|
|
||||||
|
|
||||||
# Create a direct download response with the CSV data and appropriate headers
|
|
||||||
response = Response(csv_data, content_type="text/csv; charset=utf-8")
|
|
||||||
response.headers["Content-Disposition"] = "attachment; filename=redis.csv"
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def is_over_threshold(number, total, threshold):
|
def is_over_threshold(number, total, threshold):
|
||||||
percentage = number / total * 100 if total else 0
|
percentage = number / total * 100 if total else 0
|
||||||
return percentage > threshold
|
return percentage > threshold
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ def send_messages(service_id, template_id):
|
|||||||
form = CsvUploadForm()
|
form = CsvUploadForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
|
|
||||||
upload_id = s3upload(
|
upload_id = s3upload(
|
||||||
service_id,
|
service_id,
|
||||||
Spreadsheet.from_file_form(form).as_dict,
|
Spreadsheet.from_file_form(form).as_dict,
|
||||||
@@ -167,7 +168,7 @@ def send_messages(service_id, template_id):
|
|||||||
# just show the first error, as we don't expect the form to have more
|
# just show the first error, as we don't expect the form to have more
|
||||||
# than one, since it only has one field
|
# than one, since it only has one field
|
||||||
first_field_errors = list(form.errors.values())[0]
|
first_field_errors = list(form.errors.values())[0]
|
||||||
error_message = '<span class="error-message usa-error-message">'
|
error_message = '<span class="usa-error-message">'
|
||||||
error_message = f"{error_message}{first_field_errors[0]}"
|
error_message = f"{error_message}{first_field_errors[0]}"
|
||||||
error_message = f"{error_message}</span>"
|
error_message = f"{error_message}</span>"
|
||||||
error_message = Markup(error_message)
|
error_message = Markup(error_message)
|
||||||
@@ -924,10 +925,6 @@ def get_template_error_dict(exception):
|
|||||||
def preview_notification(service_id, template_id):
|
def preview_notification(service_id, template_id):
|
||||||
recipient = get_recipient()
|
recipient = get_recipient()
|
||||||
if not recipient:
|
if not recipient:
|
||||||
current_app.logger.warning(
|
|
||||||
f"No recipient found for service {service_id}, template {template_id}. Redirecting..."
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
".send_one_off",
|
".send_one_off",
|
||||||
|
|||||||
@@ -68,12 +68,11 @@ def _get_access_token(code): # pragma: no cover
|
|||||||
id_token = get_id_token(response_json)
|
id_token = get_id_token(response_json)
|
||||||
nonce = id_token["nonce"]
|
nonce = id_token["nonce"]
|
||||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||||
if not os.getenv("NOTIFY_ENVIRONMENT") == "development":
|
stored_nonce = redis_client.get(nonce_key).decode("utf8")
|
||||||
stored_nonce = redis_client.get(nonce_key).decode("utf8")
|
|
||||||
|
|
||||||
if nonce != stored_nonce:
|
if nonce != stored_nonce:
|
||||||
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
|
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
access_token = response_json["access_token"]
|
access_token = response_json["access_token"]
|
||||||
@@ -113,7 +112,7 @@ def _do_login_dot_gov(): # $ pragma: no cover
|
|||||||
verify_key = f"login-verify_email-{unquote(state)}"
|
verify_key = f"login-verify_email-{unquote(state)}"
|
||||||
verify_path = bool(redis_client.get(verify_key))
|
verify_path = bool(redis_client.get(verify_key))
|
||||||
|
|
||||||
if not verify_path and not os.getenv("NOTIFY_ENVIRONMENT") == "development":
|
if not verify_path:
|
||||||
state_key = f"login-state-{unquote(state)}"
|
state_key = f"login-state-{unquote(state)}"
|
||||||
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
||||||
if state != stored_state:
|
if state != stored_state:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ def using_notify_nav():
|
|||||||
nav_items = [
|
nav_items = [
|
||||||
{"name": "Get started", "link": "main.get_started"},
|
{"name": "Get started", "link": "main.get_started"},
|
||||||
{
|
{
|
||||||
"name": "Best practices",
|
"name": "Best Practices",
|
||||||
"link": "main.best_practices",
|
"link": "main.best_practices",
|
||||||
"sub_navigation_items": [
|
"sub_navigation_items": [
|
||||||
{
|
{
|
||||||
@@ -33,8 +33,8 @@ def using_notify_nav():
|
|||||||
},
|
},
|
||||||
{"name": "Trial mode", "link": "main.trial_mode_new"},
|
{"name": "Trial mode", "link": "main.trial_mode_new"},
|
||||||
{"name": "Tracking usage", "link": "main.pricing"},
|
{"name": "Tracking usage", "link": "main.pricing"},
|
||||||
{"name": "Delivery status", "link": "main.message_status"},
|
{"name": "Delivery Status", "link": "main.message_status"},
|
||||||
{"name": "How to", "link": "main.how_to"},
|
{"name": "Guidance", "link": "main.guidance_index"},
|
||||||
]
|
]
|
||||||
|
|
||||||
return nav_items
|
return nav_items
|
||||||
@@ -56,6 +56,10 @@ def about_notify_nav():
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Join Notify",
|
||||||
|
"link": "main.join_notify",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Contact us",
|
"name": "Contact us",
|
||||||
"link": "main.contact",
|
"link": "main.contact",
|
||||||
|
|||||||
@@ -680,45 +680,41 @@ 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):
|
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":
|
if template.template_type == "sms":
|
||||||
s1 = (
|
s1 = f"<html><body>Use of characters outside the <a href='{url}'>IEC_8859-1</a> character set may increase "
|
||||||
"<html><body>Looks like your template may have one of these characters "
|
s2 = "the message fragment count, resulting in additional charges, and these IEC_8859-1 "
|
||||||
"• ™ ∞ ≤ or ≥ or emoji, which won't save."
|
s3 = "characters may not display properly on some older mobile devices.</body></html>"
|
||||||
)
|
|
||||||
s2 = "<br>Please remove any unaccepted characters or emojis and try again.</body></html>"
|
|
||||||
|
|
||||||
# Define characters that should be blocked
|
warning = ""
|
||||||
BLOCKED_CHARACTERS = {"•", "™", "∞", "≤", "≥"}
|
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
|
|
||||||
) # 🚨 ONLY show the warning, hiding "Will be charged..."
|
|
||||||
|
|
||||||
# If message is too long, return the length error
|
|
||||||
if template.is_message_too_long():
|
if template.is_message_too_long():
|
||||||
return True, (
|
return True, (
|
||||||
f"You have "
|
f"You have "
|
||||||
f"{character_count(template.content_count_without_prefix - SMS_CHAR_COUNT_LIMIT)} "
|
f"{character_count(template.content_count_without_prefix - SMS_CHAR_COUNT_LIMIT)} "
|
||||||
f"too many"
|
f"too many"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Show charge message as usual if no warning
|
|
||||||
if template.placeholders:
|
if template.placeholders:
|
||||||
return False, Markup(
|
return False, (
|
||||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
Markup(
|
||||||
f"(not including personalization)."
|
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} "
|
||||||
)
|
)
|
||||||
|
|
||||||
return False, Markup(
|
|
||||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)}."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class Spreadsheet:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalise_newlines(file_content):
|
def normalise_newlines(file_content):
|
||||||
return "\r\n".join(file_content.read().decode("utf-8").splitlines())
|
rows = file_content.read().decode("utf-8").splitlines()
|
||||||
|
return "\r\n".join(rows)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_rows(cls, rows, filename=""):
|
def from_rows(cls, rows, filename=""):
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ class HeaderNavigation(Navigation):
|
|||||||
"pricing",
|
"pricing",
|
||||||
"trial_mode_new",
|
"trial_mode_new",
|
||||||
"message_status",
|
"message_status",
|
||||||
"how_to",
|
"guidance_index",
|
||||||
},
|
},
|
||||||
"accounts-or-dashboard": {
|
"accounts-or-dashboard": {
|
||||||
"conversation",
|
"conversation",
|
||||||
|
"inbox",
|
||||||
|
"monthly",
|
||||||
"service_dashboard",
|
"service_dashboard",
|
||||||
"template_usage",
|
"template_usage",
|
||||||
"view_notification",
|
"view_notification",
|
||||||
@@ -159,6 +161,8 @@ class MainNavigation(Navigation):
|
|||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"conversation",
|
"conversation",
|
||||||
|
"inbox",
|
||||||
|
"monthly",
|
||||||
"service_dashboard",
|
"service_dashboard",
|
||||||
"template_usage",
|
"template_usage",
|
||||||
"view_notification",
|
"view_notification",
|
||||||
|
|||||||
@@ -1,55 +1,24 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from app.extensions import redis_client
|
|
||||||
from app.notify_client import NotifyAdminAPIClient
|
from app.notify_client import NotifyAdminAPIClient
|
||||||
|
|
||||||
|
|
||||||
class BillingAPIClient(NotifyAdminAPIClient):
|
class BillingAPIClient(NotifyAdminAPIClient):
|
||||||
def get_monthly_usage_for_service(self, service_id, year):
|
def get_monthly_usage_for_service(self, service_id, year):
|
||||||
monthly_usage = redis_client.get(f"monthly-usage-summary-{service_id}-{year}")
|
return self.get(
|
||||||
if monthly_usage is not None:
|
|
||||||
return json.loads(monthly_usage.decode("utf-8"))
|
|
||||||
result = self.get(
|
|
||||||
"/service/{0}/billing/monthly-usage".format(service_id),
|
"/service/{0}/billing/monthly-usage".format(service_id),
|
||||||
params=dict(year=year),
|
params=dict(year=year),
|
||||||
)
|
)
|
||||||
redis_client.set(
|
|
||||||
f"monthly-usage-summary-{service_id}-{year}",
|
|
||||||
json.dumps(result),
|
|
||||||
ex=30,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_annual_usage_for_service(self, service_id, year=None):
|
def get_annual_usage_for_service(self, service_id, year=None):
|
||||||
annual_usage = redis_client.get(f"yearly-usage-summary-{service_id}-{year}")
|
return self.get(
|
||||||
if annual_usage is not None:
|
|
||||||
return json.loads(annual_usage.decode("utf-8"))
|
|
||||||
result = self.get(
|
|
||||||
"/service/{0}/billing/yearly-usage-summary".format(service_id),
|
"/service/{0}/billing/yearly-usage-summary".format(service_id),
|
||||||
params=dict(year=year),
|
params=dict(year=year),
|
||||||
)
|
)
|
||||||
|
|
||||||
redis_client.set(
|
|
||||||
f"yearly-usage-summary-{service_id}-{year}",
|
|
||||||
json.dumps(result),
|
|
||||||
ex=30,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_free_sms_fragment_limit_for_year(self, service_id, year=None):
|
def get_free_sms_fragment_limit_for_year(self, service_id, year=None):
|
||||||
frag_limit = redis_client.get(f"free-sms-fragment-limit-{service_id}-{year}")
|
|
||||||
if frag_limit is not None:
|
|
||||||
return json.loads(frag_limit.decode("utf-8"))
|
|
||||||
result = self.get(
|
result = self.get(
|
||||||
"/service/{0}/billing/free-sms-fragment-limit".format(service_id),
|
"/service/{0}/billing/free-sms-fragment-limit".format(service_id),
|
||||||
params=dict(financial_year_start=year),
|
params=dict(financial_year_start=year),
|
||||||
)
|
)
|
||||||
|
|
||||||
redis_client.set(
|
|
||||||
f"free-sms-fragment-limit-{service_id}-{year}",
|
|
||||||
json.dumps(result["free_sms_fragment_limit"]),
|
|
||||||
ex=30,
|
|
||||||
)
|
|
||||||
return result["free_sms_fragment_limit"]
|
return result["free_sms_fragment_limit"]
|
||||||
|
|
||||||
def create_or_update_free_sms_fragment_limit(
|
def create_or_update_free_sms_fragment_limit(
|
||||||
@@ -67,28 +36,13 @@ class BillingAPIClient(NotifyAdminAPIClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_data_for_billing_report(self, start_date, end_date):
|
def get_data_for_billing_report(self, start_date, end_date):
|
||||||
x_start_date = str(start_date)
|
return self.get(
|
||||||
x_start_date = x_start_date.replace(" ", "_")
|
|
||||||
x_end_date = str(end_date)
|
|
||||||
x_end_date = x_end_date.replace(" ", "_")
|
|
||||||
billing_data = redis_client.get(
|
|
||||||
f"get-data-for-billing-report-{x_start_date}-{x_end_date}"
|
|
||||||
)
|
|
||||||
if billing_data is not None:
|
|
||||||
return json.loads(billing_data.decode("utf-8"))
|
|
||||||
result = self.get(
|
|
||||||
url="/platform-stats/data-for-billing-report",
|
url="/platform-stats/data-for-billing-report",
|
||||||
params={
|
params={
|
||||||
"start_date": str(start_date),
|
"start_date": str(start_date),
|
||||||
"end_date": str(end_date),
|
"end_date": str(end_date),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
redis_client.set(
|
|
||||||
f"get-data-for-billing-report-{x_start_date}-{x_end_date}",
|
|
||||||
json.dumps(result),
|
|
||||||
ex=30,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_data_for_volumes_by_service_report(self, start_date, end_date):
|
def get_data_for_volumes_by_service_report(self, start_date, end_date):
|
||||||
return self.get(
|
return self.get(
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from app.extensions import redis_client
|
|
||||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +41,7 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
|||||||
if job_id:
|
if job_id:
|
||||||
return method(
|
return method(
|
||||||
url="/service/{}/job/{}/notifications".format(service_id, job_id),
|
url="/service/{}/job/{}/notifications".format(service_id, job_id),
|
||||||
**kwargs,
|
**kwargs
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if limit_days is not None:
|
if limit_days is not None:
|
||||||
@@ -99,20 +96,9 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_notification_count_for_job_id(self, *, service_id, job_id):
|
def get_notification_count_for_job_id(self, *, service_id, job_id):
|
||||||
counts = redis_client.get(
|
return self.get(
|
||||||
f"notification-count-for-job-id-{service_id}-{job_id}"
|
|
||||||
)
|
|
||||||
if counts is not None:
|
|
||||||
return json.loads(counts.decode("utf-8"))
|
|
||||||
result = self.get(
|
|
||||||
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
|
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
|
||||||
)
|
)["count"]
|
||||||
redis_client.set(
|
|
||||||
f"notification-count-for-job-id-{service_id}-{job_id}",
|
|
||||||
json.dumps(result["count"]),
|
|
||||||
ex=30,
|
|
||||||
)
|
|
||||||
return result["count"]
|
|
||||||
|
|
||||||
|
|
||||||
notification_api_client = NotificationApiClient()
|
notification_api_client = NotificationApiClient()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from app.extensions import redis_client
|
from app.extensions import redis_client
|
||||||
@@ -518,18 +517,7 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
|||||||
return int(count)
|
return int(count)
|
||||||
|
|
||||||
def get_global_notification_count(self, service_id):
|
def get_global_notification_count(self, service_id):
|
||||||
notification_count = redis_client.get(f"notification-count-{service_id}")
|
return self.get("/service/{}/notification-count".format(service_id))
|
||||||
if notification_count is not None:
|
|
||||||
return json.loads(notification_count.decode("utf-8"))
|
|
||||||
|
|
||||||
notification_count = self.get(
|
|
||||||
"/service/{}/notification-count".format(service_id)
|
|
||||||
)
|
|
||||||
redis_client.set(
|
|
||||||
f"notification-count-{service_id}", json.dumps(notification_count), ex=30
|
|
||||||
)
|
|
||||||
|
|
||||||
return notification_count
|
|
||||||
|
|
||||||
def get_service_invite_data(self, redis_key):
|
def get_service_invite_data(self, redis_key):
|
||||||
"""
|
"""
|
||||||
@@ -537,11 +525,6 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
|||||||
"""
|
"""
|
||||||
return self.get("/service/invite/redis/{0}".format(redis_key))
|
return self.get("/service/invite/redis/{0}".format(redis_key))
|
||||||
|
|
||||||
def get_service_message_ratio(self, service_id):
|
|
||||||
return self.get(
|
|
||||||
url="service/get-service-message-ratio?service_id={0}".format(service_id),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
service_api_client = ServiceAPIClient()
|
service_api_client = ServiceAPIClient()
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class UserApiClient(NotifyAdminAPIClient):
|
|||||||
data["next"] = next_string
|
data["next"] = next_string
|
||||||
if code_type == "email":
|
if code_type == "email":
|
||||||
data["email_auth_link_host"] = self.admin_url
|
data["email_auth_link_host"] = self.admin_url
|
||||||
endpoint = f"/user/{user_id}/{code_type}-code"
|
endpoint = f"/user/{user_id}/{code_type}-code"
|
||||||
current_app.logger.warn(hilite(f"Sending verify_code {code_type} to {user_id}"))
|
current_app.logger.warn(hilite(f"Sending verify_code {code_type} to {user_id}"))
|
||||||
self.post(endpoint, data=data)
|
self.post(endpoint, data=data)
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||||
{% block bodyStart %}
|
{% block bodyStart %}
|
||||||
{% block extra_javascripts_before_body %}
|
{% block extra_javascripts_before_body %}
|
||||||
|
<!-- Google Tag Manager (noscript) -->
|
||||||
<noscript><iframe 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>
|
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||||
|
<!-- End Google Tag Manager (noscript) -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -29,30 +31,6 @@
|
|||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% include 'components/usa_banner.html' %}
|
{% include 'components/usa_banner.html' %}
|
||||||
{% 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 %}
|
|
||||||
{% include 'components/header.html' %}
|
{% include 'components/header.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -118,10 +96,10 @@
|
|||||||
{% block footer %}
|
{% block footer %}
|
||||||
|
|
||||||
{% if current_service and current_service.research_mode %}
|
{% 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 %}
|
{% else %}
|
||||||
{% set commit_hash = ", Latest version: " + config['COMMIT_HASH'] %}
|
{% 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 %}
|
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -167,8 +145,10 @@
|
|||||||
{% block bodyEnd %}
|
{% block bodyEnd %}
|
||||||
{% block extra_javascripts %}
|
{% block extra_javascripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
<!--[if gt IE 8]><!-->
|
||||||
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||||
|
<!--<![endif]-->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
{#- Define common attributes we can use for both button and input types #}
|
{#- Define common attributes we can use for both button and input types #}
|
||||||
|
|
||||||
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name | trim }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
||||||
|
|
||||||
{#- Actually create a button... or a link! #}
|
{#- Actually create a button... or a link! #}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
classes: params.label.classes,
|
classes: params.label.classes,
|
||||||
isPageHeading: params.label.isPageHeading,
|
isPageHeading: params.label.isPageHeading,
|
||||||
attributes: params.label.attributes,
|
attributes: params.label.attributes,
|
||||||
for: params.text
|
for: params.id
|
||||||
}) | indent(2) | trim }}
|
}) | indent(2) | trim }}
|
||||||
{% if params.hint %}
|
{% if params.hint %}
|
||||||
{% set hintId = params.id + '-hint' %}
|
{% set hintId = params.id + '-hint' %}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
}) | indent(2) | trim }}
|
}) | indent(2) | trim }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if params.errorMessage %}
|
{% if params.errorMessage %}
|
||||||
{% set errorId = params.label.text + '-error' %}
|
{% set errorId = params.id + '-error' %}
|
||||||
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
||||||
{{ usaErrorMessage({
|
{{ usaErrorMessage({
|
||||||
id: errorId,
|
id: errorId,
|
||||||
@@ -34,19 +34,13 @@
|
|||||||
attributes: params.errorMessage.attributes,
|
attributes: params.errorMessage.attributes,
|
||||||
html: params.errorMessage.html,
|
html: params.errorMessage.html,
|
||||||
text: params.errorMessage.text,
|
text: params.errorMessage.text,
|
||||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
|
visuallyHiddenText: params.errorMessage.visuallyHiddenText
|
||||||
}) | indent(2) | trim }}
|
}) | indent(2) | trim }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input
|
<input class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}" id="{{ params.id }}" name="{{ params.name }}" type="{{ params.type | default('text') }}"
|
||||||
class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}"
|
{%- if params.value %} value="{{ params.value}}"{% endif %}
|
||||||
id="{{ params.label.text | default('unknown') | slugify }}"
|
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||||
name="{{ params.name }}"
|
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete}}"{% endif %}
|
||||||
type="{{ params.type | default('text') }}"
|
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
|
||||||
{%- if params.value %} value="{{ params.value }}"{% endif %}
|
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
|
||||||
{%- 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 -%}
|
|
||||||
{%- if params.required %} required{% endif %}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if field.errors and show_errors %}
|
{% if field.errors and show_errors %}
|
||||||
<span class="error-message usa-error-message">
|
<span class="error-message">
|
||||||
{{ field.errors[0] }}
|
{{ field.errors[0] }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
class=None,
|
class=None,
|
||||||
id=None,
|
id=None,
|
||||||
module=None,
|
module=None,
|
||||||
data_kwargs={},
|
data_kwargs={}
|
||||||
data_force_focus=False
|
|
||||||
) %}
|
) %}
|
||||||
<form
|
<form
|
||||||
method="{{ method }}"
|
method="{{ method }}"
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
data-{{ key }}="{{ val }}"
|
data-{{ key }}="{{ val }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data_force_focus %}data-force-focus="{{ data_force_focus }}"{% endif %}
|
|
||||||
novalidate
|
novalidate
|
||||||
>
|
>
|
||||||
{{ caller() }}
|
{{ caller() }}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% set is_about_page = request.path.startswith('/about') %}
|
{% 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_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 %}
|
{% if current_user.is_authenticated %}
|
||||||
{% set navigation = [
|
{% set navigation = [
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set navigation = [
|
{% set navigation = [
|
||||||
{"href": url_for('main.about_notify'), "text": "About Notify", "active": is_about_page},
|
{"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}
|
{"href": url_for('main.contact'), "text": "Contact us", "active": is_contact_page}
|
||||||
] %}
|
] %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<legend class="form-label {% if bold_legend %}bold{% endif %}">
|
<legend class="form-label {% if bold_legend %}bold{% endif %}">
|
||||||
{{ field.label.text }}
|
{{ field.label.text }}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
<span class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
<span class="error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||||
{{ field.errors[0] }}
|
{{ field.errors[0] }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
<span class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
<span class="error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||||
{{ field.errors[0] }}
|
{{ field.errors[0] }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -16,9 +16,19 @@
|
|||||||
placeholder=''
|
placeholder=''
|
||||||
) %}
|
) %}
|
||||||
<div
|
<div
|
||||||
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
|
class="form-group{% if field.errors %} form-group-error{% endif %} {{ extra_form_group_classes }}"
|
||||||
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
|
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
|
||||||
>
|
>
|
||||||
|
{% if field.errors %}
|
||||||
|
<div class="usa-alert usa-alert--error edit-textbox-error-mt" role="alert">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
<h4 class="usa-alert__heading">Error message</h4>
|
||||||
|
<p class="usa-alert__text" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
|
||||||
|
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<label class="usa-label" for="{{ field.name }}">
|
<label class="usa-label" for="{{ field.name }}">
|
||||||
{% if label %}
|
{% if label %}
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -31,12 +41,6 @@
|
|||||||
{{ hint }}
|
{{ hint }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if field.errors %}
|
|
||||||
<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 %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{%
|
{%
|
||||||
if highlight_placeholders or autosize
|
if highlight_placeholders or autosize
|
||||||
%}
|
%}
|
||||||
@@ -55,8 +59,6 @@
|
|||||||
data_highlight_placeholders='true' if highlight_placeholders else 'false',
|
data_highlight_placeholders='true' if highlight_placeholders else 'false',
|
||||||
rows=rows|string,
|
rows=rows|string,
|
||||||
placeholder=placeholder,
|
placeholder=placeholder,
|
||||||
aria_describedby=field.name+"-error",
|
|
||||||
required='required' if required else None,
|
|
||||||
**kwargs
|
**kwargs
|
||||||
) }}
|
) }}
|
||||||
{% if suffix %}
|
{% if suffix %}
|
||||||
|
|||||||
@@ -21,11 +21,11 @@
|
|||||||
|
|
||||||
<div class="ajax-block-container">
|
<div class="ajax-block-container">
|
||||||
<p class='bottom-gutter'>
|
<p class='bottom-gutter'>
|
||||||
{% if not job.finished_processing %}
|
{% if job.still_processing or arrived_from_preview_page_url %}
|
||||||
{% if job.scheduled_for %}
|
{% if job.scheduled_for %}
|
||||||
<div class="usa-alert usa-alert--info">
|
<div class="usa-alert usa-alert--info">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<h2 class="usa-alert__heading">Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} been scheduled</h2>
|
<h2 class="usa-alert__heading">Your text has been scheduled</h2>
|
||||||
<p class="usa-alert__text">
|
<p class="usa-alert__text">
|
||||||
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
{{ job.template_name }} - {{ current_service.name }} was scheduled on {{ job.scheduled_for|format_datetime_normal }} by {{ job.created_by.name }}
|
||||||
</p>
|
</p>
|
||||||
@@ -33,46 +33,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{{display_message_status}}
|
{{display_message_status}}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if job.processing_started %}
|
<div class="usa-alert usa-alert--success">
|
||||||
<div class="usa-alert usa-alert--success">
|
<div class="usa-alert__body">
|
||||||
<div class="usa-alert__body">
|
<h2 class="usa-alert__heading">Your text has been sent</h2>
|
||||||
<h2 class="usa-alert__heading">
|
<p class="usa-alert__text">
|
||||||
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} sending
|
{{ job.template_name }} - {{ current_service.name }} was sent on {% if job.processing_started %}
|
||||||
</h2>
|
{{ job.processing_started|format_datetime_table }} {% else %}
|
||||||
<p class="usa-alert__text">
|
{{ job.created_at|format_datetime_table }} {% endif %} by {{ job.created_by.name }}
|
||||||
{{ job.template_name }} - {{ current_service.name }}
|
</p>
|
||||||
has been sending since {{job.processing_started| format_datetime_normal}} by {{ job.created_by.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="usa-alert usa-alert--info">
|
|
||||||
<div class="usa-alert__body">
|
|
||||||
<h2 class="usa-alert__heading">
|
|
||||||
Your {{ 'message is' if job.notification_count == 1 else 'messages are' }} pending
|
|
||||||
</h2>
|
|
||||||
<p class="usa-alert__text">
|
|
||||||
{{ job.template_name }} - {{ current_service.name }}
|
|
||||||
has been pending since {{job.created_at|format_datetime_normal}} by {{ job.created_by.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{{display_message_status}}
|
{{display_message_status}}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif arrived_from_preview_page_url %}
|
|
||||||
<div class="usa-alert usa-alert--success">
|
|
||||||
<div class="usa-alert__body">
|
|
||||||
<h2 class="usa-alert__heading">
|
|
||||||
Your {{ 'message has' if job.notification_count == 1 else 'messages have' }} been sent
|
|
||||||
</h2>
|
|
||||||
<p class="usa-alert__text">
|
|
||||||
{{ job.template_name }} - {{ current_service.name }}
|
|
||||||
was sent on {{job.processing_started|format_datetime_normal}} by {{ job.created_by.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{display_message_status}}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% if job.status == 'sending limits exceeded'%}
|
{% if job.status == 'sending limits exceeded'%}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div id="message-length" class="usa-accordion__content usa-prose">
|
<div id="message-length" class="usa-accordion__content usa-prose">
|
||||||
<p class="usa-body">
|
<p class="usa-body">
|
||||||
Each message sent to a recipient counts as one text. Keep track on your Dashboard of how many messages have sent and are remaining.
|
If your message is long then it will
|
||||||
|
cost more.
|
||||||
|
</p>
|
||||||
|
<p class="usa-body">
|
||||||
|
See <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a> for details.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div id="m-a2" class="usa-accordion__content usa-prose">
|
<div id="m-a2" class="usa-accordion__content usa-prose">
|
||||||
<p class="usa-body">
|
<p class="usa-body">
|
||||||
Use double parenthesis and ‘??’ to define optional content.
|
Use double brackets and ‘??’ to define optional content.
|
||||||
</p>
|
</p>
|
||||||
<p class="bottom-gutter-1-3">
|
<p class="bottom-gutter-1-3">
|
||||||
For example if you only want to show something to people who are under
|
For example if you only want to show something to people who are under
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div id="personalization" class="usa-accordion__content usa-prose">
|
<div id="personalization" class="usa-accordion__content usa-prose">
|
||||||
<p class="bottom-gutter-1-3">
|
<p class="bottom-gutter-1-3">
|
||||||
Use double parenthesis to personalize your message:
|
Use double brackets to personalize your message:
|
||||||
</p>
|
</p>
|
||||||
{{ usaInsetText({
|
{{ usaInsetText({
|
||||||
"text": "Hello ((first name)), your reference is ((ref number))",
|
"text": "Hello ((first name)), your reference is ((ref number))",
|
||||||
"classes": ""})
|
"classes": ""})
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
Send a document by email
|
Send a document by email
|
||||||
</h2>
|
</h2>
|
||||||
<p class="usa-body">
|
<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>
|
</p>
|
||||||
{{ usaInsetText({
|
{{ usaInsetText({
|
||||||
"text": "Download your document at: ((link_to_file))",
|
"text": "Download your document at: ((link_to_file))",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% set page_title = "About Notify" %}
|
{% set page_title = "About Notify" %}
|
||||||
|
|
||||||
{% block per_page_title %}
|
{% block per_page_title %}
|
||||||
{{page_title}}
|
{{page_title}}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
{% set product_highlights = [
|
{% set product_highlights = [
|
||||||
{
|
{
|
||||||
"svg_src": "#send",
|
"svg_src": "#send",
|
||||||
"card_heading": "Send customized one-way messages",
|
"card_heading": "Send customized one-way customized messages",
|
||||||
"p_text": "Upload a file with recipient phone numbers and Notify.gov sends customized messages",
|
"p_text": "Upload a file with recipient phone numbers and Notify.gov sends customized messages",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</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
|
<p>Notify.gov is a product of the <a href="/studio">Public Benefits Studio</a>, a product accelerator inside
|
||||||
the federal government. </p>
|
the federal government. </p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -40,7 +40,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Texting not only helps programs reach people using a nearly-universal communication method, it is a cost effective
|
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>
|
</p>
|
||||||
<h2 id="what-texting-is-best-for">What texting is best for</h2>
|
<h2 id="what-texting-is-best-for">What texting is best for</h2>
|
||||||
<p>
|
<p>
|
||||||
@@ -50,37 +51,35 @@
|
|||||||
{
|
{
|
||||||
"image_src": asset_url('images/calendar.svg'),
|
"image_src": asset_url('images/calendar.svg'),
|
||||||
"card_heading": "Reminders",
|
"card_heading": "Reminders",
|
||||||
"p_text": "Your Quality Control food phone interview is on ((date)) at ((time)). Failure to
|
"p_text": "In a text bubble // Your Quality Control food phone interview is on ((date)) at ((time)). Failure to
|
||||||
attend may lead to closure of your benefits. Call 1-800-222-3333 with questions.",
|
attend may lead to closure of your benefits. Call 1-800-222-3333 with questions.",
|
||||||
"alt_text": "reminder text example"
|
"alt_text": "reminder text example"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image_src": asset_url('images/alert.svg'),
|
"image_src": asset_url('images/alert.svg'),
|
||||||
"card_heading": "Alerts to take action",
|
"card_heading": "Alerts to take action",
|
||||||
"p_text": "Your household's Medicaid coverage is expiring. To keep getting Medicaid, you must
|
"p_text": "In a text bubble // Your household's Medicaid coverage is expiring. To keep getting Medicaid, you must
|
||||||
complete your renewal by ((date)). You can renew online at dhs.state.gov…",
|
complete your renewal by ((date)). You can renew online at dhs.state.gov…",
|
||||||
"alt_text": "alerts text example"
|
"alt_text": "alerts text example"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image_src": asset_url('images/alarm.svg'),
|
"image_src": asset_url('images/alarm.svg'),
|
||||||
"card_heading": "Important status updates",
|
"card_heading": "Important status updates",
|
||||||
"p_text": "Your passport has been issued at the Los Angeles Passport Agency. Please come to the
|
"p_text": "In a text bubble // Your passport has been issued at the Los Angeles Passport Agency. Please come to the
|
||||||
desk between 1:30pm and 2:30pm today to pick up your passport…",
|
desk between 1:30pm and 2:30pm today to pick up your passport…",
|
||||||
"alt_text": "status update text example"
|
"alt_text": "status update text example"
|
||||||
},
|
},
|
||||||
] %}
|
] %}
|
||||||
{% for item in card_contents %}
|
{% for item in card_contents %}
|
||||||
<div class="grid-container-tablet padding-3 radius-lg border-2px margin-left-0 ">
|
<div class="radius-lg border-2px maxw-tablet">
|
||||||
<h3 class="usa-card__heading padding-y-2">{{item.card_heading}}</h3>
|
<div class="grid-row grid-gap-4 padding-2 padding-x-3 flex-align-center">
|
||||||
<div class="grid-row grid-gap-3 flex-align-center">
|
<div class="grid-col flex-3">
|
||||||
<div class="grid-col-auto ">
|
<p><b>{{item.card_heading}}</b></p>
|
||||||
<p class="sms-message-wrapper">{{item.p_text}}</p>
|
<p>{{item.p_text}}</p>
|
||||||
</div>
|
|
||||||
<div class="grid-col-fill">
|
|
||||||
{% if item.image_src %}
|
|
||||||
<img src="{{ item.image_src }}" alt="{{ item.alt_text }}" class="height-15" />
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if item.image_src %}
|
||||||
|
<img src="{{item.image_src}}" alt="{{ item.alt_text }}" class="height-15" />
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
{{ page_header('About your service') }}
|
{{ 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"}}) }}
|
{{ form.name(param_extensions={"hint": {"text": "You can change this later"}}) }}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ Error
|
|||||||
<div class="usa-alert usa-alert--error" role="alert">
|
<div class="usa-alert usa-alert--error" role="alert">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<h1 class="usa-alert__heading banner-title" data-module="track-error" data-error-type="Missing placeholder columns"
|
<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">
|
<p class="usa-alert__text">
|
||||||
Your file is missing {{ recipients.missing_column_headers | formatted_list(
|
Your file is missing {{ recipients.missing_column_headers | formatted_list(
|
||||||
conjunction='and',
|
conjunction='and',
|
||||||
|
|||||||
@@ -101,9 +101,6 @@
|
|||||||
{% set button_text %}
|
{% set button_text %}
|
||||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||||
{% endset %}
|
{% endset %}
|
||||||
{{ usaButton({
|
{{ usaButton({ "text": button_text }) }}
|
||||||
"text": button_text,
|
|
||||||
"name": button_text
|
|
||||||
}) }}
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,10 +7,19 @@
|
|||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>{{page_title}}</h1>
|
<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>
|
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
|
<p>You can expect a response within one business day.</p>
|
||||||
at <a href="mailto:notify-support@gsa.gov"
|
<div class="usa-summary-box maxw-tablet __web-inspector-hide-shortcut__" role="region"
|
||||||
aria-label="Email Notify for technical questions at notify-support@gsa.gov">notify-support@gsa.gov</a></p>
|
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>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block backLink %}
|
{% block backLink %}
|
||||||
{{ usaBackLink({ "href": url_for("main.service_dashboard", service_id=current_service.id) }) }}
|
{{ usaBackLink({ "href": url_for("main.inbox", service_id=current_service.id) }) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|||||||
17
app/templates/views/dashboard/_inbox.html
Normal file
17
app/templates/views/dashboard/_inbox.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<div class="ajax-block">
|
||||||
|
{% if current_service.inbound_sms_summary != None %}
|
||||||
|
<a id="total-received" class="usa-link banner-dashboard" class="banner-dashboard" href="{{ url_for('.inbox', service_id=current_service.id) }}">
|
||||||
|
<span class="banner-dashboard-count">
|
||||||
|
{{ current_service.inbound_sms_summary.count|format_thousands }}
|
||||||
|
</span>
|
||||||
|
<span class="banner-dashboard-count-label">
|
||||||
|
{{ current_service.inbound_sms_summary.count|message_count_label('sms', suffix='received') }}
|
||||||
|
</span>
|
||||||
|
{% if current_service.inbound_sms_summary.most_recent %}
|
||||||
|
<span class="banner-dashboard-meta">
|
||||||
|
latest message {{ current_service.inbound_sms_summary.most_recent | format_delta }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
38
app/templates/views/dashboard/_inbox_messages.html
Normal file
38
app/templates/views/dashboard/_inbox_messages.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{% from "components/table.html" import list_table, field, hidden_field_heading, right_aligned_field_heading, row_heading %}
|
||||||
|
{% from "components/previous-next-navigation.html" import previous_next_navigation %}
|
||||||
|
|
||||||
|
<div class="ajax-block-container">
|
||||||
|
{% if messages %}
|
||||||
|
<p class="bottom-gutter-2-3 top-gutter-1-2">
|
||||||
|
<a href="{{ url_for('.inbox_download', service_id=current_service.id) }}" download class="usa-link bold">Download these messages</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% call(item, row_number) list_table(
|
||||||
|
messages,
|
||||||
|
caption="Inbox",
|
||||||
|
caption_visible=False,
|
||||||
|
empty_message='When users text your service’s phone number ({}) you’ll see the messages here'.format(inbound_number),
|
||||||
|
field_headings=[
|
||||||
|
'From',
|
||||||
|
'First two lines of message'
|
||||||
|
],
|
||||||
|
field_headings_visible=False
|
||||||
|
) %}
|
||||||
|
{% call field() %}
|
||||||
|
<a
|
||||||
|
class="usa-link file-list-filename"
|
||||||
|
href="{{ url_for('.conversation', service_id=current_service.id, notification_id=item.id) }}#n{{ item.id }}"
|
||||||
|
>
|
||||||
|
{{ item.user_number | format_phone_number_human_readable }}
|
||||||
|
</a>
|
||||||
|
<span class="file-list-hint">{{ item.content }}</span>
|
||||||
|
{% endcall %}
|
||||||
|
{% call field(align='right') %}
|
||||||
|
<span class="align-with-message-body">
|
||||||
|
{{ item.created_at | format_delta }}
|
||||||
|
</span>
|
||||||
|
{% endcall %}
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{{ previous_next_navigation(prev_page, next_page) }}
|
||||||
|
</div>
|
||||||
25
app/templates/views/dashboard/_totals.html
Normal file
25
app/templates/views/dashboard/_totals.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="ajax-block-container">
|
||||||
|
<div class="grid-row grid-gap">
|
||||||
|
<div id="total-sms" class="grid-col-12 margin-top-2">
|
||||||
|
<span class="big-number-with-status display-block margin-bottom-2">
|
||||||
|
<p>
|
||||||
|
<span class="big-number-smaller">
|
||||||
|
<span class="big-number-number">
|
||||||
|
{% if statistics['sms']['requested'] is number %}
|
||||||
|
{{ "{:,}".format(statistics['sms']['requested']) }}
|
||||||
|
{% else %}
|
||||||
|
{{ statistics['sms']['requested'] }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span class="big-number-label">{{ statistics['sms']['requested']|message_count_label('sms', suffix='sent') }} in the last seven days</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<a class="usa-button usa-button--outline" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='sending,delivered,failed') }}">
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
{# Removing the failures area for now, as the user can click on the above link to see all the details.
|
||||||
|
In the future state of the dashboard, the all statuses will be more apparent with data visualizations #}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Recent activity</h2>
|
|
||||||
<div id="activityChartContainer">
|
|
||||||
<form class="usa-form">
|
|
||||||
<label class="usa-label" for="options">Account</label>
|
|
||||||
<select class="usa-select margin-bottom-2" name="options" id="options">
|
|
||||||
<option value disabled>- Select -</option>
|
|
||||||
<option value="service" selected>{{ current_service.name }}</option>
|
|
||||||
<option value="individual">{{ current_user.name }}</option>
|
|
||||||
</select>
|
|
||||||
</form>
|
|
||||||
<div id="activityChart">
|
|
||||||
<div class="chart-header">
|
|
||||||
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</div>
|
|
||||||
<div class="chart-legend" role="region" aria-label="Legend"></div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-container" id="weeklyChart"></div>
|
|
||||||
<table id="weeklyTable" class="usa-sr-only usa-table"></table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
|
||||||
<div class="table-container" id="activityContainer" data-currentUserName="{{ current_user.name }}" data-currentServiceId="{{current_service.id}}">
|
|
||||||
<div id="tableActivity" class="table-overflow-x-auto">
|
|
||||||
<h2 id="table-heading" class="margin-top-4 margin-bottom-1">Service activity</h2>
|
|
||||||
|
|
||||||
<table class="usa-table job-table" id="activity-table">
|
|
||||||
<caption class="usa-sr-only">Table showing the sent jobs for {{current_service.name}}</caption>
|
|
||||||
|
|
||||||
<thead class="table-field-headings">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="table-field-heading-first" id="jobId">Job ID#</th>
|
|
||||||
<th data-sortable scope="col" class="table-field-heading" scope="col">Template</th>
|
|
||||||
<th data-sortable scope="col" class="table-field-heading">Job status</th>
|
|
||||||
<th data-sortable scope="col" role="columnheader" class="table-field-heading sender-column">Sender
|
|
||||||
</th>
|
|
||||||
<th data-sortable scope="col" class="table-field-heading"># of Recipients</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% if jobs %}
|
|
||||||
{% for job in jobs %}
|
|
||||||
<tr id="{{ job.id }}">
|
|
||||||
<td class="table-field jobid" role="rowheader">
|
|
||||||
<a class="usa-link" href="{{ url_for('.view_job', service_id=current_service.id, job_id=job.id )}}">
|
|
||||||
{{ job.id[:8] if job.id else 'Manually entered number' }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="table-field template">{{ job.template_name }}</td>
|
|
||||||
<td class="table-field time-sent">
|
|
||||||
{% if not job.finished_processing %}
|
|
||||||
{% if job.scheduled_for%}
|
|
||||||
Scheduled for {{ job.scheduled_for|format_datetime_table }}
|
|
||||||
{% elif job.processing_started %}
|
|
||||||
Sending since {{ job.processing_started|format_datetime_table }}
|
|
||||||
{% else %}
|
|
||||||
Pending since {{ job.created_at|format_datetime_table }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
Sent on {{ job.processing_started|format_datetime_table }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="table-field sender sender-column">{{ job.created_by.name }}</td>
|
|
||||||
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<tr class="table-row">
|
|
||||||
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are
|
|
||||||
kept for {{ service_data_retention_days }} days).</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,52 +1,145 @@
|
|||||||
|
|
||||||
|
|
||||||
{% extends "withnav_template.html" %}
|
{% extends "withnav_template.html" %}
|
||||||
|
|
||||||
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading,
|
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading, hidden_field_heading, row_heading, notification_status_field, notification_carrier_field, notification_carrier_message_field %}
|
||||||
hidden_field_heading, row_heading, notification_status_field, notification_carrier_field,
|
|
||||||
notification_carrier_message_field %}
|
|
||||||
{% from "components/ajax-block.html" import ajax_block %}
|
{% from "components/ajax-block.html" import ajax_block %}
|
||||||
|
|
||||||
{% block service_page_title %}
|
{% block service_page_title %}
|
||||||
Dashboard
|
Dashboard
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
|
||||||
|
|
||||||
|
|
||||||
<div class="dashboard margin-top-0 margin-bottom-2">
|
<div class="dashboard margin-top-0 margin-bottom-2">
|
||||||
|
|
||||||
<h1 class="usa-sr-only">Dashboard</h1>
|
<h1 class="usa-sr-only">Dashboard</h1>
|
||||||
{% if current_user.has_permissions('manage_templates') and not current_service.all_templates %}
|
{% if current_user.has_permissions('manage_templates') and not current_service.all_templates %}
|
||||||
{% include 'views/dashboard/write-first-messages.html' %}
|
{% include 'views/dashboard/write-first-messages.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include 'views/dashboard/_upcoming.html' %}
|
{{ ajax_block(partials, updates_url, 'upcoming') }}
|
||||||
|
|
||||||
<h2 class="font-body-2xl line-height-sans-2 margin-top-0">{{ current_service.name }} Dashboard</h2>
|
<h2 class="font-body-2xl line-height-sans-2 margin-top-0">{{ current_service.name }} Dashboard</h2>
|
||||||
|
|
||||||
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent }}" data-messages-remaining="{{ messages_remaining }}">
|
{{ ajax_block(partials, updates_url, 'inbox') }}
|
||||||
<div class="grid-row flex-align-center">
|
|
||||||
<h2 id="chartTitle" class="margin-right-1">Total messages</h2>
|
<div id="totalMessageChartContainer" data-sms-sent="{{ sms_sent }}" data-sms-allowance-remaining="{{ sms_allowance_remaining }}">
|
||||||
<button
|
<h2 id="chartTitle">Total messages</h2>
|
||||||
type="button"
|
<svg id="totalMessageChart"></svg>
|
||||||
class="usa-tooltip usa-tooltip__information margin-right-0"
|
<div id="message"></div>
|
||||||
data-position="top"
|
|
||||||
title="Total messages track the sum of messages for the service: pending, failed, or delivered"
|
|
||||||
>
|
|
||||||
<span class="usa-sr-only">More information</span>
|
|
||||||
i
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<svg id="totalMessageChart"></svg>
|
|
||||||
<div id="message"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="totalMessageTable" class="margin-0"></div>
|
<div id="totalMessageTable" class="margin-0"></div>
|
||||||
{% include 'views/dashboard/activity-table.html' %}
|
|
||||||
|
<h2 class="line-height-sans-2 margin-bottom-0 margin-top-4">Recent activity</h2>
|
||||||
|
<div id="activityChartContainer">
|
||||||
|
<form class="usa-form">
|
||||||
|
<label class="usa-label" for="options">Account</label>
|
||||||
|
<select class="usa-select margin-bottom-2" name="options" id="options">
|
||||||
|
<option value disabled>- Select -</option>
|
||||||
|
<option value="service" selected>{{ current_service.name }}</option>
|
||||||
|
<option value="individual">{{ current_user.name }}</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<div id="activityChart">
|
||||||
|
<div class="chart-header">
|
||||||
|
<div class="chart-subtitle">{{ current_service.name }} - last 7 days</div>
|
||||||
|
<div class="chart-legend" role="region" aria-label="Legend"></div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container" id="weeklyChart"></div>
|
||||||
|
<table id="weeklyTable" class="usa-sr-only usa-table"></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="aria-live-account" class="usa-sr-only" aria-live="polite"></div>
|
||||||
|
|
||||||
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
||||||
|
|
||||||
{% include 'views/dashboard/most-used-templates.html' %}
|
<div class="table-container">
|
||||||
|
<div id="table1" class="table-overflow-x-auto hidden">
|
||||||
|
<h2 class="margin-top-4 margin-bottom-1">My activity</h2>
|
||||||
|
<table class="usa-table job-table">
|
||||||
|
<caption class="usa-sr-only">Table showing the sent jobs for {{current_user.name}}</caption>
|
||||||
|
<thead class="table-field-headings">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||||
|
<th data-sortable scope="col" class="table-field-heading"><span>Template</span></th>
|
||||||
|
<th data-sortable scope="col" class="table-field-heading"><span>Job status</span></th>
|
||||||
|
<th data-sortable scope="col" class="table-field-heading"><span># of Recipients</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if jobs %}
|
||||||
|
{% for job in jobs[:5] %}
|
||||||
|
{% if job.created_by.name == current_user.name %}
|
||||||
|
{% set notification = job.notifications[0] %}
|
||||||
|
<tr id="{{ job.job_id }}">
|
||||||
|
<td class="table-field jobid" role="rowheader">
|
||||||
|
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||||
|
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="table-field template">{{ job.template_name }}</td>
|
||||||
|
<td class="table-field time-sent">Sent on
|
||||||
|
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||||
|
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||||
|
</td>
|
||||||
|
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr class="table-row">
|
||||||
|
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<div id="table2" class="table-overflow-x-auto visible">
|
||||||
|
<h2 class="margin-top-4 margin-bottom-1">Service activity</h2>
|
||||||
|
<table class="usa-table job-table">
|
||||||
|
<caption class="usa-sr-only">Table showing the sent jobs for this service</caption>
|
||||||
|
<thead class="table-field-headings">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" role="columnheader" class="table-field-heading-first" id="jobId"><span>Job ID#</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Template</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Job status</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span>Sender</span></th>
|
||||||
|
<th data-sortable scope="col" role="columnheader" class="table-field-heading"><span># of Recipients</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if jobs %}
|
||||||
|
{% for job in jobs[:5] %}
|
||||||
|
{% set notification = job.notifications[0] %}
|
||||||
|
<tr id="{{ job.job_id }}">
|
||||||
|
<td class="table-field jobid" role="rowheader">
|
||||||
|
<a class="usa-link" href="{{ job.view_job_link }}">
|
||||||
|
{{ job.job_id[:8] if job.job_id else 'Manually entered number' }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="table-field template">{{ job.template_name }}</td>
|
||||||
|
<td class="table-field time-sent">Sent on
|
||||||
|
{{ (job.processing_finished if job.processing_finished else job.processing_started
|
||||||
|
if job.processing_started else job.created_at)|format_datetime_table }}
|
||||||
|
</td>
|
||||||
|
<td class="table-field sender">{{ job.created_by.name }}</td>
|
||||||
|
<td class="table-field count-of-recipients">{{ job.notification_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr class="table-row">
|
||||||
|
<td class="table-empty-message" colspan="10">No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
25
app/templates/views/dashboard/inbox.html
Normal file
25
app/templates/views/dashboard/inbox.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "withnav_template.html" %}
|
||||||
|
{% from "components/ajax-block.html" import ajax_block %}
|
||||||
|
{% from "components/page-header.html" import page_header %}
|
||||||
|
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||||
|
|
||||||
|
{% block service_page_title %}
|
||||||
|
Received text messages
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block backLink %}
|
||||||
|
{{ usaBackLink({ "href": url_for('main.service_dashboard', service_id=current_service.id) }) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
|
{{ page_header('Received text messages') }}
|
||||||
|
|
||||||
|
{{ ajax_block(
|
||||||
|
partials,
|
||||||
|
updates_url,
|
||||||
|
'messages',
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -34,13 +34,12 @@
|
|||||||
<td><p>{{ item.template_folder }}</p></td>
|
<td><p>{{ item.template_folder }}</p></td>
|
||||||
<td><p>{{ item.last_used|format_datetime_table}}</p></td>
|
<td><p>{{ item.last_used|format_datetime_table}}</p></td>
|
||||||
<td><p>{{ item.created_by }}</p></td>
|
<td><p>{{ item.created_by }}</p></td>
|
||||||
<td><p>{{ '{:,.0f}'.format(item.count) }}</p></td>
|
<td><p>{{ item.count }}</p></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<a
|
<a href="/services/78409625-0c0a-485e-b82c-b19c8f4b1bdb/template-usage" class="usa-link show-more-no-border"><span>See templates by month</span></a>
|
||||||
href="{{ url_for('.template_usage', service_id=current_service.id) }}" class="usa-link show-more-no-border"><span>See templates by month</span></a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -27,13 +27,11 @@
|
|||||||
{% set content_hint = 'Your service name will be added to the start of your message. You can turn this off in Settings.' %}
|
{% set content_hint = 'Your service name will be added to the start of your message. You can turn this off in Settings.' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
<div class="grid-container padding-0">
|
<div class="grid-row">
|
||||||
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
|
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
|
||||||
{{ form.name(param_extensions={
|
{{ form.name(param_extensions={
|
||||||
"extra_form_group_classes": "margin-bottom-2",
|
"extra_form_group_classes": "margin-bottom-2",
|
||||||
"id": "name",
|
|
||||||
"required": True,
|
|
||||||
"hint": {"text": "Your recipients will not see this"}
|
"hint": {"text": "Your recipients will not see this"}
|
||||||
}) }}
|
}) }}
|
||||||
{{ textbox(
|
{{ textbox(
|
||||||
@@ -43,8 +41,7 @@
|
|||||||
hint=content_hint,
|
hint=content_hint,
|
||||||
rows=5,
|
rows=5,
|
||||||
extra_form_group_classes='margin-bottom-1',
|
extra_form_group_classes='margin-bottom-1',
|
||||||
placeholder='Edit me! Check out the Personalization section below for details on cool ((stuff)) you can do with your messages!',
|
placeholder='Edit me! Check out the Personalization section below for details on cool ((stuff)) you can do with your messages!'
|
||||||
required=True
|
|
||||||
) }}
|
) }}
|
||||||
{% if current_user.platform_admin %}
|
{% if current_user.platform_admin %}
|
||||||
{{ form.process_type }}
|
{{ form.process_type }}
|
||||||
@@ -61,12 +58,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="tablet:grid-col-2 mobile-lg:grid-col-12">
|
||||||
{{ page_footer('Save') }}
|
{{ page_footer('Save') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tablet:grid-col-10 mobile-lg:grid-col-12">
|
<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.
|
After saving, you'll have the option to send.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<h2 class="heading-medium" id="personalised-messages">Personalized content</h2>
|
<h2 class="heading-medium" id="personalised-messages">Personalized content</h2>
|
||||||
<p class="usa-body">Notify makes it easy to send personalized messages from a single template.</p>
|
<p class="usa-body">Notify makes it easy to send personalized messages from a single template.</p>
|
||||||
<p class="usa-body">See <a class="usa-link" href="{{ url_for('.how_to', _anchor='personalized-content') }}">how to personalize your content</a>.</p>
|
<p class="usa-body">See <a class="usa-link" href="{{ url_for('.guidance_index', _anchor='personalized-content') }}">how to personalize your content</a>.</p>
|
||||||
|
|
||||||
<h2 class="heading-medium" id="bulk-sending">Bulk sending</h2>
|
<h2 class="heading-medium" id="bulk-sending">Bulk sending</h2>
|
||||||
<p class="usa-body">To send a batch of messages at once, upload a list of contact details to Notify. You can also schedule the date and time you want them to be sent.</p>
|
<p class="usa-body">To send a batch of messages at once, upload a list of contact details to Notify. You can also schedule the date and time you want them to be sent.</p>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<ol class="list list-number">
|
<ol class="list list-number">
|
||||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
<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 new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||||
<li>Add a placeholder using double parenthesis. For example: Hello ((first name)), your reference is ((ref number)).</li>
|
<li>Add a placeholder using double brackets. For example: Hello ((first name)), your reference is ((ref number)).</li>
|
||||||
<li>Select <b class="bold">Save</b>.</li>
|
<li>Select <b class="bold">Save</b>.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<ol class="list list-number">
|
<ol class="list list-number">
|
||||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
<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 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>
|
<li>Select <b class="bold">Save</b>.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
{% from "components/service-link.html" import service_link %}
|
{% from "components/service-link.html" import service_link %}
|
||||||
|
|
||||||
{% block per_page_title %}
|
{% block per_page_title %}
|
||||||
How to
|
Guidance
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
<h1 class="font-body-2xl margin-bottom-3">How to</h1>
|
<h1 class="font-body-2xl margin-bottom-3">Guidance</h1>
|
||||||
|
|
||||||
<p>Notify allows you to easily create templates for messages for your recipients. You can customize messages to encourage
|
<p>Notify allows you to easily create templates for messages for your recipients. You can customize messages to encourage
|
||||||
your recipient to manage their benefits and increase follow-through.</p>
|
your recipient to manage their benefits and increase follow-through.</p>
|
||||||
@@ -59,9 +59,9 @@ your recipient to manage their benefits and increase follow-through.</p>
|
|||||||
|
|
||||||
<h3>To personalize your content</h3>
|
<h3>To personalize your content</h3>
|
||||||
<ol class="list">
|
<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
|
<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>
|
work for you. See <a href="#prepare-data">data preparation</a>.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
@@ -78,9 +78,9 @@ all or part of the message contingent upon specific criteria associated with the
|
|||||||
|
|
||||||
<h3>To add conditional content</h3>
|
<h3>To add conditional content</h3>
|
||||||
<ol class="list">
|
<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
|
<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>
|
do the work for you. See <a href="#prepare-data">data preparation</a>.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<h4>Examples</h4>
|
<h4>Examples</h4>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
{{ breadcrumbs.breadcrumb(page_title, "Best practices", "main.best_practices") }}
|
{{ breadcrumbs.breadcrumb(page_title, "Best Practices", "main.best_practices") }}
|
||||||
|
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page_title}}</h1>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% set page_title = "Best practices" %}
|
{% set page_title = "Best Practices" %}
|
||||||
|
|
||||||
{% block per_page_title %}
|
{% block per_page_title %}
|
||||||
{{page_title}}
|
{{page_title}}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>Best practices</h1>
|
<h1>Best Practices</h1>
|
||||||
<p class="font-sans-lg text-base">For texting the public</p>
|
<p class="font-sans-lg text-base">For texting the public</p>
|
||||||
<p>Effectively reaching your audience and supporting your program’s goals starts with strategically planning out what
|
<p>Effectively reaching your audience and supporting your program’s goals starts with strategically planning out what
|
||||||
text messages can help you achieve and how to approach a thoughtful rollout.
|
text messages can help you achieve and how to approach a thoughtful rollout.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
{{ breadcrumbs.breadcrumb(page_title, "Best practices", "main.best_practices") }}
|
{{ breadcrumbs.breadcrumb(page_title, "Best Practices", "main.best_practices") }}
|
||||||
|
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page_title}}</h1>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
{{ breadcrumbs.breadcrumb(page_title, "Best practices", "main.best_practices") }}
|
{{ breadcrumbs.breadcrumb(page_title, "Best Practices", "main.best_practices") }}
|
||||||
|
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page_title}}</h1>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
{{ breadcrumbs.breadcrumb(page_title, "Best practices", "main.best_practices") }}
|
{{ breadcrumbs.breadcrumb(page_title, "Best Practices", "main.best_practices") }}
|
||||||
|
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page_title}}</h1>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
{{ breadcrumbs.breadcrumb(page_title, "Best practices", "main.best_practices") }}
|
{{ breadcrumbs.breadcrumb(page_title, "Best Practices", "main.best_practices") }}
|
||||||
|
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page_title}}</h1>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_column_content %}
|
{% block content_column_content %}
|
||||||
{{ breadcrumbs.breadcrumb(page_title, "Best practices", "main.best_practices") }}
|
{{ breadcrumbs.breadcrumb(page_title, "Best Practices", "main.best_practices") }}
|
||||||
|
|
||||||
<section class="usa-prose">
|
<section class="usa-prose">
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page_title}}</h1>
|
||||||
|
|||||||
@@ -11,18 +11,7 @@
|
|||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
{{ page_header("Message status") }}
|
{{ page_header("Message status") }}
|
||||||
{% if not job.finished_processing %}
|
{{ partials['status']|safe }}
|
||||||
<div
|
|
||||||
data-module="update-content"
|
|
||||||
data-resource="{{ updates_url }}"
|
|
||||||
data-key="status"
|
|
||||||
data-form=""
|
|
||||||
>
|
|
||||||
{% endif %}
|
|
||||||
{{ partials['status']|safe }}
|
|
||||||
{% if not job.finished_processing %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if not finished %}
|
{% if not finished %}
|
||||||
<div
|
<div
|
||||||
data-module="update-content"
|
data-module="update-content"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
</div>
|
</div>
|
||||||
{% elif error == 'message-too-long' %}
|
{% 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">
|
<div class="bottom-gutter">
|
||||||
{% call banner_wrapper(type='dangerous') %}
|
{% call banner_wrapper(type='dangerous') %}
|
||||||
{% include "partials/check/message-too-long.html" %}
|
{% include "partials/check/message-too-long.html" %}
|
||||||
@@ -76,15 +77,13 @@
|
|||||||
help='3' if help else 0
|
help='3' if help else 0
|
||||||
)}}" class='page-footer'>
|
)}}" class='page-footer'>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<!-- <p>Placeholder: This message will be delivered to <b>400 phone numbers</b> and will use a total of <b>800 message parts</b>, leaving Washington DSHS with <b>249,200 message parts remaining</b>.</p> -->
|
||||||
<h3>Does everything look good?</h3>
|
<h3>Does everything look good?</h3>
|
||||||
{% if not error %}
|
{% if not error %}
|
||||||
{% set button_text %}
|
{% set button_text %}
|
||||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||||
{% endset %}
|
{% endset %}
|
||||||
{{ usaButton({
|
{{ usaButton({ "text": button_text }) }}
|
||||||
"text": button_text,
|
|
||||||
"name": button_text
|
|
||||||
}) }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% set page_title = "Notify.gov Service Ending" %}
|
|
||||||
|
|
||||||
{% block per_page_title %}{{page_title}}{% endblock %}
|
|
||||||
|
|
||||||
{% block content_column_content %}
|
|
||||||
|
|
||||||
<section class="usa-prose">
|
|
||||||
<h1>{{page_title}}</h1>
|
|
||||||
<p>GSA will no longer offer the Notify.gov service after June 8th, 2025. Current partners will continue to have service access until it is fully suspended. After May 30, partners will no longer be able to send messages, and the full service will be suspended by June 8, 2025.
|
|
||||||
</p>
|
|
||||||
<h2>Notify.gov Status Announcement</h2>
|
|
||||||
<p class="text-italic">Notify.gov was in beta and will no longer be offered by GSA. Operations will be suspended on or before June 8. There is a chance that the service may need to stop sooner. GSA will keep partners informed to the extent possible.</p>
|
|
||||||
|
|
||||||
<p class="text-italic">All Notify.gov partners will be moved to trial mode no later than May 30, and will no longer be able to send messages. Partners will be able to access their templates and data through June 8.</p>
|
|
||||||
|
|
||||||
<p class="text-italic">We encourage government programs to explore ways to continue utilizing technology to meet people where they are. There are several commercial tools on the market that may work for Notify.gov use cases.</p>
|
|
||||||
|
|
||||||
<p class="text-italic">GSA is working to avoid potential disruptions to partners’ operations and ensure a smooth transition.</p>
|
|
||||||
|
|
||||||
<p class="text-italic">This decision is part of GSA’s renewed focus on its original mission: to be the backbone of the federal government’s administrative operations — streamlining processes, optimizing resources, and ensuring cost-effectiveness. GSA is driven to reducing federal spend, contributing to a more flexible and streamlined government, and delivering value for the American people.</p>
|
|
||||||
|
|
||||||
<p class="text-italic">We wish you all the best as you continue your work. </p>
|
|
||||||
|
|
||||||
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -34,8 +34,5 @@
|
|||||||
<p>
|
<p>
|
||||||
<a class="usa-link" href="{{ url_for('main.download_all_users') }}">Download All Users</a>
|
<a class="usa-link" href="{{ url_for('main.download_all_users') }}">Download All Users</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
<a class="usa-link" href="{{ url_for('main.get_redis_report') }}">Get Redis Report</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -34,14 +34,11 @@
|
|||||||
{% call form_wrapper(
|
{% call form_wrapper(
|
||||||
class='js-stick-at-top-when-scrolling send-one-off-form' if template.template_type != 'sms' else 'send-one-off-form',
|
class='js-stick-at-top-when-scrolling send-one-off-form' if template.template_type != 'sms' else 'send-one-off-form',
|
||||||
module="autofocus",
|
module="autofocus",
|
||||||
data_kwargs={'force-focus': True},
|
data_kwargs={'force-focus': True}
|
||||||
data_force_focus=True
|
|
||||||
) %}
|
) %}
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
{% set extra_class = "extra-tracking" if form.placeholder_value.label.text == "phone number" else "" %}
|
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}" aria-live="polite" role="alert">
|
||||||
{% set placeholder_id = "phone number" if form.placeholder_value.label.text == "phone number" else "" %}
|
{{ form.placeholder_value(param_extensions={"classes": ""}) }}
|
||||||
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
|
|
||||||
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
|
|
||||||
</div>
|
</div>
|
||||||
{% if skip_link or link_to_upload %}
|
{% if skip_link or link_to_upload %}
|
||||||
<div class="grid-col-12 margin-top-1">
|
<div class="grid-col-12 margin-top-1">
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="font-body-lg">Your file needs to look like this example</h2>
|
<h2 class="font-body-lg">Your file needs to look like this example</h2>
|
||||||
|
<p class="hint">
|
||||||
|
Save your file as a
|
||||||
|
<abbr title="Comma Separated Values">CSV</abbr><span aria-hidden="true">, </span>
|
||||||
|
<abbr title="Tab Separated Values">TSV</abbr><span aria-hidden="true">, </span>
|
||||||
|
<abbr title="Open Document Spreadsheet">ODS</abbr>
|
||||||
|
or Microsoft Excel spreadsheet.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="spreadsheet" data-module="fullscreen-table">
|
<div class="spreadsheet" data-module="fullscreen-table">
|
||||||
{% call(item, row_number) list_table(
|
{% call(item, row_number) list_table(
|
||||||
@@ -43,14 +50,10 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">
|
|
||||||
Save your spreadsheet as a <abbr title="Comma Separated Values">CSV</abbr> file for bulk messaging. It is the most reliable when uploading your contact list. Start by downloading this example for your message template.
|
|
||||||
</p>
|
|
||||||
<p class="table-show-more-link">
|
<p class="table-show-more-link">
|
||||||
<a class="usa-link display-flex margin-top-1" href="{{ url_for('.get_example_csv', service_id=current_service.id, template_id=template.id) }}" download>Download this example (<abbr title="Comma separated values">CSV</abbr>)
|
<a class="usa-link" href="{{ url_for('.get_example_csv', service_id=current_service.id, template_id=template.id) }}" download>Download this example (<abbr title="Comma separated values">CSV</abbr>)</a>
|
||||||
<img class="margin-left-05" src="{{ asset_url('img/material-icons/download.svg') }}" alt="" />
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="font-body-lg margin-bottom-1">Your file will populate this template:<br><span class="font-body-lg">({{ template.name }})</span></h2>
|
<h2 class="font-body-lg margin-bottom-1">Your file will populate this template:<br><span class="font-body-lg">({{ template.name }})</span></h2>
|
||||||
{{ template|string }}
|
{{ template|string }}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
label='Search by name',
|
label='Search by name',
|
||||||
autofocus=True
|
autofocus=True
|
||||||
) }}
|
) }}
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{% if has_organizations %}
|
{% if has_organizations %}
|
||||||
{{ form.organizations }}
|
{{ form.organizations }}
|
||||||
{{ sticky_page_footer('Save') }}
|
{{ sticky_page_footer('Save') }}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{{ page_header('Free text message allowance') }}
|
{{ page_header('Free text message allowance') }}
|
||||||
{{ form.free_sms_allowance }}
|
{{ form.free_sms_allowance }}
|
||||||
{{ page_footer('Save') }}
|
{{ page_footer('Save') }}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
See <a class="usa-link" href="{{ url_for(".pricing") }}">pricing</a> for the list
|
See <a class="usa-link" href="{{ url_for(".pricing") }}">pricing</a> for the list
|
||||||
of rates.
|
of rates.
|
||||||
</p>
|
</p>
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{{ form.enabled }}
|
{{ form.enabled }}
|
||||||
{{ page_footer('Save') }}
|
{{ page_footer('Save') }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{{ page_header('Message batch limit') }}
|
{{ page_header('Message batch limit') }}
|
||||||
{{ form.message_limit }}
|
{{ form.message_limit }}
|
||||||
{{ page_footer('Save') }}
|
{{ page_footer('Save') }}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{{ page_header('Rate limit') }}
|
{{ page_header('Rate limit') }}
|
||||||
{{ form.rate_limit }}
|
{{ form.rate_limit }}
|
||||||
{{ page_footer('Save') }}
|
{{ page_footer('Save') }}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
<div class="grid-col-10">
|
<div class="grid-col-10">
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{{ form.enabled(param_extensions={
|
{{ form.enabled(param_extensions={
|
||||||
"fieldset": {
|
"fieldset": {
|
||||||
"legend": {
|
"legend": {
|
||||||
|
|||||||
@@ -20,7 +20,12 @@
|
|||||||
<p>
|
<p>
|
||||||
You may send up to 250,000 text messages during the pilot period.
|
You may send up to 250,000 text messages during the pilot period.
|
||||||
</p>
|
</p>
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
<!--<p>
|
||||||
|
You have a free allowance of
|
||||||
|
{{ '{:,}'.format(current_service.free_sms_fragment_limit) }} text messages each
|
||||||
|
financial year.
|
||||||
|
</p>-->
|
||||||
|
{% call form_wrapper() %}
|
||||||
{{ form.enabled }}
|
{{ form.enabled }}
|
||||||
{{ page_footer('Save') }}
|
{{ page_footer('Save') }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
{{ page_header('Start text messages with service name') }}
|
{{ page_header('Start text messages with service name') }}
|
||||||
|
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{{ form.enabled }}
|
{{ form.enabled }}
|
||||||
{{ page_footer('Save') }}
|
{{ page_footer('Save') }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
{{ page_header('Add text message sender') }}
|
{{ page_header('Add text message sender') }}
|
||||||
|
|
||||||
{% call form_wrapper(data_force_focus=True) %}
|
{% call form_wrapper() %}
|
||||||
{{ form.sms_sender(param_extensions={
|
{{ form.sms_sender(param_extensions={
|
||||||
"hint": {"text": "Up to 11 characters, letters, numbers and spaces only"}
|
"hint": {"text": "Up to 11 characters, letters, numbers and spaces only"}
|
||||||
}) }}
|
}) }}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user