mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-22 23:36:26 -04:00
Compare commits
4 Commits
01-29-2024
...
ADMIN-96_E
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
363ea9e744 | ||
|
|
59419706be | ||
|
|
8d6f62cde1 | ||
|
|
e8aa3158a5 |
71
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
71
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: "Bug Report"
|
||||
description: "Use this template to file a bug report"
|
||||
title: "Bug: [Brief description of the bug]"
|
||||
|
||||
labels:
|
||||
- "Type: Bug"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please use this form to file new bugs! Add screenshots to the comments of this initial form.
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Who discovered this?
|
||||
description: Was it something you found or are you filing for someone else? If someone else, how can we contact them?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: In which environment did you experience the bug?
|
||||
options:
|
||||
- staging
|
||||
- demo
|
||||
- production
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: page
|
||||
attributes:
|
||||
label: What pages did this appear on?
|
||||
description: please include URLs of where you saw the bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproSteps
|
||||
attributes:
|
||||
label: "Detail the steps for someone to reproduce"
|
||||
description: "Reproduction steps"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
76
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
76
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
@@ -1,76 +0,0 @@
|
||||
name: "User Story Template"
|
||||
description: "Use this template for creating user stories"
|
||||
title: "User Story: [Brief description of the user story]"
|
||||
|
||||
labels:
|
||||
- "Type: User Story"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**User Story:**'
|
||||
- type: textarea
|
||||
id: userType
|
||||
attributes:
|
||||
label: "As a [type of user],"
|
||||
description: "Describe the type of user involved in this story."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actionFeature
|
||||
attributes:
|
||||
label: "I want [an action or feature],"
|
||||
description: "Describe the desired action or feature from the user's perspective."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: benefitValue
|
||||
attributes:
|
||||
label: "So that [benefit or value]."
|
||||
description: "Describe the benefit or value the user expects from the action or feature."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Acceptance Criteria:**'
|
||||
- type: textarea
|
||||
id: acceptanceCriteria
|
||||
attributes:
|
||||
label: "Detailed condition or criteria that must be met for the user story to be considered complete."
|
||||
description: "List the acceptance criteria for the user story."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Tasks:**'
|
||||
- type: textarea
|
||||
id: tasks
|
||||
attributes:
|
||||
label: "List of specific tasks or sub-tasks that need to be done to implement the user story."
|
||||
description: "Outline the tasks necessary to implement the user story."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Dependencies:**'
|
||||
- type: textarea
|
||||
id: dependencies
|
||||
attributes:
|
||||
label: "List any dependencies that need to be resolved before starting or completing this user story."
|
||||
description: "Specify any dependencies related to the user story."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Notes:**'
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: "Any additional information or context that might be useful for the team."
|
||||
description: "Provide any extra notes or context for the user story."
|
||||
validations:
|
||||
required: false
|
||||
82
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
82
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: "QA Checklist"
|
||||
description: "Use this template to manually QA for prod push"
|
||||
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: Logging_in
|
||||
attributes:
|
||||
label: Log In
|
||||
options:
|
||||
- label: notify-staging.app.cloud.gov resolves
|
||||
- label: Clicking sign in takes you to login page
|
||||
- label: 2FA code is sent promptly
|
||||
- label: 2FA code input screen appears as expected
|
||||
- label: After code is entered, you are brought to the “choose service” screen
|
||||
- label: Choosing a service brings you to the dashboard for that service
|
||||
|
||||
- type: checkboxes
|
||||
id: send_template
|
||||
attributes:
|
||||
label: Create a template and send
|
||||
options:
|
||||
- label: Clicking “Send messages” brings you to templates page
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Uploading a csv is quick and automatically directs you to the preview/send page
|
||||
- label: Formatting looks clean and in place
|
||||
- label: Message dashboard populates and shows “pending” until call is made to AWS for delivery receipt
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__copied_template
|
||||
attributes:
|
||||
label: Send a message from copied template
|
||||
options:
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Clicking “copy an existing template” allows you to choose from pre-existing template
|
||||
- label: After editing, the new template saves and you prepare to send
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__existing_template
|
||||
attributes:
|
||||
label: Send a message from existing template
|
||||
options:
|
||||
- label: Clicking on existing template brings you to preview screen with `prepare to send` button
|
||||
- label: All else above persists as true (formatting, send, dashboard, etc)
|
||||
|
||||
- type: checkboxes
|
||||
id: download_report
|
||||
attributes:
|
||||
label: Download the csv delivery report
|
||||
options:
|
||||
- label: Clicking on the csv download directly after a message send downloads a report that contains the data we expect
|
||||
- label: The batch report appears on the main dashboard batch table with accurate information
|
||||
- label: Clicking on the csv download link from the main dashboard downloads a report that contains the data we expect
|
||||
|
||||
- type: checkboxes
|
||||
id: invite_member
|
||||
attributes:
|
||||
label: Invite a team member
|
||||
options:
|
||||
- label: Clicking on “Team” in settings brings you to team member screen
|
||||
- label: Clicking “Invite a team member” brings you to email and permissions screen
|
||||
- label: Clicking invite team member sends an invite and shows confirmation
|
||||
- label: Invite email provides live link that works to register new team member
|
||||
|
||||
- type: checkboxes
|
||||
id: delete_member
|
||||
attributes:
|
||||
label: Delete a team member
|
||||
options:
|
||||
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that person’s profile
|
||||
- label: Clicking on “Remove this team member” prompts a confirmation
|
||||
- label: Clicking the confirmation removes the team member
|
||||
|
||||
- type: checkboxes
|
||||
id: sign_out
|
||||
attributes:
|
||||
label: Sign Out
|
||||
options:
|
||||
- label: Hitting “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
6
.github/workflows/deploy-demo.yml
vendored
6
.github/workflows/deploy-demo.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
with:
|
||||
files: |
|
||||
terraform/demo
|
||||
@@ -54,7 +54,6 @@ jobs:
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
LOGIN_PEM: ${{ secrets.LOGIN_PEM }}
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -68,11 +67,10 @@ jobs:
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-demo.*.acl
|
||||
|
||||
7
.github/workflows/deploy-prod.yml
vendored
7
.github/workflows/deploy-prod.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
with:
|
||||
files: |
|
||||
terraform/production
|
||||
@@ -54,8 +54,6 @@ jobs:
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
LOGIN_PEM: ${{ secrets.LOGIN_PEM }}
|
||||
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -69,11 +67,10 @@ jobs:
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-production.*.acl
|
||||
|
||||
22
.github/workflows/deploy.yml
vendored
22
.github/workflows/deploy.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
with:
|
||||
files: |
|
||||
terraform/staging
|
||||
@@ -51,7 +51,6 @@ jobs:
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: 18f/cg-deploy-action@main
|
||||
env:
|
||||
@@ -60,14 +59,6 @@ jobs:
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
LOGIN_PEM: ${{ secrets.LOGIN_PEM }}
|
||||
LOGIN_DOT_GOV_CLIENT_ID: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov"
|
||||
LOGIN_DOT_GOV_USER_INFO_URL: "https://idp.int.identitysandbox.gov/api/openid_connect/userinfo"
|
||||
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://idp.int.identitysandbox.gov/api/openid_connect/token"
|
||||
LOGIN_DOT_GOV_LOGOUT_URL: "https://idp.int.identitysandbox.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://idp.int.identitysandbox.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://idp.int.identitysandbox.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=01234567890123456789012345&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=abcdefghijklmnopabcdefghijklmnop"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
@@ -81,19 +72,10 @@ jobs:
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
--var LOGIN_DOT_GOV_LOGOUT_URL="$LOGIN_DOT_GOV_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-staging.*.acl
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -127,8 +127,6 @@ terraform.tfstate.backup
|
||||
# Playwright
|
||||
playwright/
|
||||
|
||||
# Pyenv
|
||||
# Pyenv/Nodenv
|
||||
.python-version
|
||||
|
||||
# Nodenv
|
||||
.node-version
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: debug-statements
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
3
.profile
3
.profile
@@ -4,6 +4,3 @@
|
||||
##
|
||||
|
||||
export NEW_RELIC_PROXY_HOST=$egress_proxy
|
||||
export http_proxy=$egress_proxy
|
||||
export https_proxy=$egress_proxy
|
||||
export no_proxy="apps.internal"
|
||||
|
||||
5
Makefile
5
Makefile
@@ -15,11 +15,8 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: generate-version-file ## Set up everything to run the app
|
||||
poetry self update
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry install --sync --no-root
|
||||
poetry install --sync
|
||||
poetry run playwright install --with-deps
|
||||
poetry run pre-commit install
|
||||
source $(NVMSH) --no-use && nvm install && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
|
||||
455
README.md
455
README.md
@@ -1,8 +1,6 @@
|
||||
# Notify.gov Admin UI
|
||||
# Notify UI
|
||||
|
||||
This is the Notify front-end for government users and admins. To see it in
|
||||
action, check out [the demo site](https://notify-demo.app.cloud.gov)
|
||||
(contact team for credentials).
|
||||
This is the Notify front-end for government users and admins. To see it in action, check out [the demo site](https://notify-demo.app.cloud.gov) (contact team for credentials).
|
||||
|
||||
Through the interface, users can:
|
||||
|
||||
@@ -11,372 +9,69 @@ Through the interface, users can:
|
||||
- Send batch SMS by uploading a CSV
|
||||
- View their history of notifications
|
||||
|
||||
The [Notify.gov API](https://github.com/GSA/notifications-api) provides the
|
||||
UI's backend and is required for most things to function. Set that up first!
|
||||
The [Notify API](https://github.com/GSA/notifications-api) provides the UI's backend and is required for most things to function. Set that up first!
|
||||
|
||||
Our other repositories are:
|
||||
## Local setup
|
||||
|
||||
- [notifications-admin](https://github.com/GSA/notifications-admin)
|
||||
- [notifications-utils](https://github.com/GSA/notifications-utils)
|
||||
- [us-notify-compliance](https://github.com/GSA/us-notify-compliance/)
|
||||
- [notify-python-demo](https://github.com/GSA/notify-python-demo)
|
||||
### Common steps
|
||||
|
||||
## Before You Start
|
||||
1. Install pre-requisites for setup (on a Mac):
|
||||
- Install XCode or at least the XCode Command Line Tools
|
||||
- [Homebrew](https://brew.sh/) (follow instructions on page)
|
||||
- [jq](https://stedolan.github.io/jq/): `brew install jq`
|
||||
- [terraform](https://www.terraform.io/): `brew install terraform` or `brew install tfenv` and use `tfenv` to install `terraform ~> 1.4.0`
|
||||
- [cf-cli@8](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html): `brew install cloudfoundry/tap/cf-cli@8`
|
||||
1. [Log into cloud.gov](https://cloud.gov/docs/getting-started/setup/#set-up-the-command-line): `cf login -a api.fr.cloud.gov --sso`
|
||||
1. Ensure you have access to the `notify-local-dev` and `notify-staging` spaces in cloud.gov
|
||||
1. Run the API setup steps
|
||||
1. Run the development terraform with:
|
||||
|
||||
You will need the following items:
|
||||
```
|
||||
$ cd terraform/development
|
||||
$ ./run.sh
|
||||
```
|
||||
|
||||
- An active cloud.gov account with the correct permissions - speak with your
|
||||
onboarding buddy for help with
|
||||
[setting up an account](https://cloud.gov/sign-up/) (requires a `.mil`,
|
||||
`.gov`, or `.fed.us` email address) and getting access to the
|
||||
`notify-local-dev` and `notify-staging` spaces.
|
||||
- Admin priviliges and SSH access on your machine; you may need to work with
|
||||
your organization's IT support staff if you're not sure or don't currently
|
||||
have this access.
|
||||
1. If you want to send data to New Relic from your local develpment environment, set `NEW_RELIC_LICENSE_KEY` within `.env`
|
||||
1. Follow the instructions for either `Direct installation` or `Docker installation` below
|
||||
|
||||
**In addition, you should set up the Notify.gov API first!**
|
||||
### Direct installation
|
||||
|
||||
[Follow the instructions here to set up the Notify.gov API.](https://github.com/GSA/notifications-api#before-you-start)
|
||||
1. Get the API running
|
||||
|
||||
The Notify.gov API is required in order for the Notify.gov Admin UI to run, and
|
||||
it will also take care of many of the steps that are listed here. The sections
|
||||
that are a repeat from the API setup are flagged with an **[API Step]** label
|
||||
in front of them.
|
||||
1. Install [poetry](https://python-poetry.org/docs/#installation)
|
||||
|
||||
## Local Environment Setup
|
||||
1. Install [nvm (Node Version Manager)](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
|
||||
This project is set up to work with
|
||||
[nvm (Node Version Manager)](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
for managing and using Node.js (version 16.15.1) and the `npm` package manager.
|
||||
1. Install Python and Node dependencies
|
||||
|
||||
These instructions will walk you through how to set your machine up with all of
|
||||
the required tools for this project.
|
||||
`make bootstrap`
|
||||
|
||||
If you run into certificate errors at the `playwright install` step, try doing this:
|
||||
|
||||
1. Run `brew --prefix` to see Homebrew's root directory
|
||||
|
||||
1. Create or modify the local `.env` file in the project and add this line:
|
||||
|
||||
`NODE_EXTRA_CA_CERTS=/CHANGE-TO-HOMEBREW-INSTALL-PATH/etc/ca-certificates/cert.pem`
|
||||
|
||||
Make sure to change `CHANGE-TO-HOMEBREW-INSTALL-PATH` to the path given by `brew --prefix` in the step above.
|
||||
For example, if `brew --prefix` gave `/opt/homebrew` as output, then the line would look like this:
|
||||
|
||||
`NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem`
|
||||
|
||||
1. Save the changes to the `.env` file
|
||||
|
||||
1. Run `make bootstrap` again
|
||||
|
||||
1. Run the Flask server
|
||||
|
||||
`make run-flask`
|
||||
|
||||
1. Go to http://localhost:6012
|
||||
|
||||
### Docker installation
|
||||
|
||||
If you are using VS Code, there are also instructions for
|
||||
[running inside Docker](./docs/docker-remote-containers.md).
|
||||
|
||||
### [API Step] Project Pre-Requisite Setup
|
||||
|
||||
On MacOS, using [Homebrew](https://brew.sh/) for package management is highly
|
||||
recommended. This helps avoid some known installation issues. Start by following
|
||||
the installation instructions on the Homebrew homepage.
|
||||
|
||||
**Note:** You will also need Xcode or the Xcode Command Line Tools installed. The
|
||||
quickest way to do this is is by installing the command line tools in the shell:
|
||||
|
||||
```sh
|
||||
xcode-select –-install
|
||||
```
|
||||
|
||||
#### [API Step] Homebrew Setup
|
||||
|
||||
If this is your first time installing Homebrew on your machine, you may need to
|
||||
add its binaries to your system's `$PATH` environment variable so that you can
|
||||
use the `brew` command. Try running `brew help` to see if Homebrew is
|
||||
recognized and runs properly. If that fails, then you'll need to add a
|
||||
configuration line to wherever your `$PATH` environment variable is set.
|
||||
|
||||
Your system `$PATH` environment variable is likely set in one of these
|
||||
locations:
|
||||
|
||||
For BASH shells:
|
||||
- `~/.bashrc`
|
||||
- `~/.bash_profile`
|
||||
- `~/.profile`
|
||||
|
||||
For ZSH shells:
|
||||
- `~/.zshrc`
|
||||
- `~/.zprofile`
|
||||
|
||||
There may be different files that you need to modify for other shell
|
||||
environments.
|
||||
|
||||
Which file you need to modify depends on whether or not you are running an
|
||||
interactive shell or a login shell
|
||||
(see [this Stack Overflow post](https://stackoverflow.com/questions/18186929/what-are-the-differences-between-a-login-shell-and-interactive-shell)
|
||||
for an explanation of the differences). If you're still not sure, please ask
|
||||
the team for help!
|
||||
|
||||
Once you determine which file you'll need to modify, add these lines before any
|
||||
lines that add or modify the `$PATH` environment variable; near or at the top
|
||||
of the file is appropriate:
|
||||
|
||||
```sh
|
||||
# Homebrew setup
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
```
|
||||
|
||||
This will make sure Homebrew gets setup correctly. Once you make these changes,
|
||||
either start a new shell session or source the file
|
||||
(`source ~/.FILE-YOU-MODIFIED`) you modified to have your system recognize the
|
||||
changes.
|
||||
|
||||
Verify that Homebrew is now working by trying to run `brew help` again.
|
||||
|
||||
### [API Step] System-Level Package Installation
|
||||
|
||||
There are several packages you will need to install for your system in order to
|
||||
get the app running (and these are good to have in general for any software
|
||||
development).
|
||||
|
||||
Start off with these packages since they're quick and don't require additional
|
||||
configuration after installation to get working out of the box:
|
||||
|
||||
- [jq](https://stedolan.github.io/jq/) - for working with JSON in the command
|
||||
line
|
||||
- [git](https://git-scm.com/) - for version control management
|
||||
- [tfenv](https://github.com/tfutils/tfenv) - for managing
|
||||
[Terraform](https://www.terraform.io/) installations
|
||||
- [cf-cli@8](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) - for
|
||||
working with a Cloud Foundry platform (e.g., cloud.gov)
|
||||
- [vim](https://www.vim.org/) - for editing files more easily in the command
|
||||
line
|
||||
- [wget](https://www.gnu.org/software/wget/) - for retrieving files in the
|
||||
command line
|
||||
|
||||
You can install them by running the following:
|
||||
|
||||
```sh
|
||||
brew install jq git tfenv cloudfoundry/tap/cf-cli@8 vim wget
|
||||
```
|
||||
|
||||
#### [API Step] Terraform Installation
|
||||
|
||||
As a part of the installation above, you just installed `tfenv` to manage
|
||||
Terraform installations. This is great, but you still need to install Terraform
|
||||
itself, which can be done with this command:
|
||||
|
||||
```sh
|
||||
tfenv install latest:^1.4.0
|
||||
```
|
||||
|
||||
_NOTE: This project currently uses the latest `1.4.x release of Terraform._
|
||||
|
||||
#### [API Step] Python Installation
|
||||
|
||||
Now we're going to install a tool to help us manage Python versions and
|
||||
virtual environments on our system. First, we'll install
|
||||
[pyenv](https://github.com/pyenv/pyenv) and one of its plugins,
|
||||
[pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv), with Homebrew:
|
||||
|
||||
```sh
|
||||
brew install pyenv pyenv-virtualenv
|
||||
```
|
||||
|
||||
When these finish installing, you'll need to make another adjustment in the
|
||||
file that you adjusted for your `$PATH` environment variable and Homebrew's
|
||||
setup. Open the file, and add these lines to it:
|
||||
|
||||
```
|
||||
# pyenv setup
|
||||
export PYENV_ROOT="$HOME/.pyenv"
|
||||
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
```
|
||||
|
||||
Once again, start a new shell session or source the file in your current shell
|
||||
session to make the changes take effect.
|
||||
|
||||
Now we're ready to install the Python version we need with `pyenv`, like so:
|
||||
|
||||
```sh
|
||||
pyenv install 3.9
|
||||
```
|
||||
|
||||
This will install the latest version of Python 3.9.
|
||||
|
||||
_NOTE: This project currently runs on Python 3.9.x._
|
||||
|
||||
#### [API Step] Python Dependency Installation
|
||||
|
||||
Lastly, we need to install the tool we use to manage Python dependencies within
|
||||
the project, which is [poetry](https://python-poetry.org/).
|
||||
|
||||
Visit the
|
||||
[official installer instructions page](https://python-poetry.org/docs/#installing-with-the-official-installer)
|
||||
and follow the steps to install Poetry directly with the script.
|
||||
|
||||
This will ensure `poetry` doesn't conflict with any project virtual environments
|
||||
and can update itself properly.
|
||||
|
||||
#### Node.js and npm Installation
|
||||
|
||||
The project will manage most of the Node.js pieces for you, but you will need to
|
||||
install
|
||||
[nvm (Node Version Manager)](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
in order for things to work.
|
||||
|
||||
Follow the steps in the installation instructions to get `nvm` installed with
|
||||
the install script they provide, and double check that the file that you
|
||||
adjusted for your `$PATH` environment variable and Homebrew's setup is properly
|
||||
updated with the `nvm` setup as well.
|
||||
|
||||
Open the file and check for the `nvm` setup lines; if they're not there, then
|
||||
add the lines found in the
|
||||
[nvm installation instructions](https://github.com/nvm-sh/nvm#install--update-script)
|
||||
to to the file after the `pyenv` section you created, e.g.:
|
||||
|
||||
```
|
||||
# nvm setup
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
|
||||
```
|
||||
|
||||
### First-Time Project Setup
|
||||
|
||||
Once all of pre-requisites for the project are installed and you have a
|
||||
cloud.gov account, you can now set up the admin project and get things running
|
||||
locally!
|
||||
|
||||
First, clone the respository in the directory of your choosing on your machine:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:GSA/notifications-admin.git
|
||||
```
|
||||
|
||||
Now go into the project directory (`notifications-admin` by default), create a
|
||||
virtual environment, and set the local Python version to point to the virtual
|
||||
environment (assumes version Python `3.9.18` is what is installed on your
|
||||
machine):
|
||||
|
||||
```sh
|
||||
cd notifications-admin
|
||||
pyenv virtualenv 3.9.18 notify-admin
|
||||
pyenv local notify-admin
|
||||
```
|
||||
|
||||
_If you're not sure which version of Python was installed with `pyenv`, you can check by running `pyenv versions` and it'll list everything available currently._
|
||||
|
||||
Now [log into cloud.gov](https://cloud.gov/docs/getting-started/setup/#set-up-the-command-line)
|
||||
in the command line by using this command:
|
||||
|
||||
```sh
|
||||
cf login -a api.fr.cloud.gov --sso
|
||||
```
|
||||
|
||||
_REMINDER: Ensure you have access to the `notify-local-dev` and `notify-staging` spaces in cloud.gov_
|
||||
|
||||
Now run the development Terraform setup by navigating to the development
|
||||
folder and running the script in it:
|
||||
|
||||
```sh
|
||||
cd terraform/development
|
||||
./run.sh
|
||||
```
|
||||
|
||||
In addition to some infrastructure setup, this will also create a local `.env`
|
||||
file for you in the project's root directory, which will include a handful of
|
||||
project-specific environment variables.
|
||||
|
||||
#### Updating the .env file for E2E tests
|
||||
|
||||
With the newly created `.env` file in place, you'll need to make one more
|
||||
adjustment in it for things to run properly for the E2E tests.
|
||||
|
||||
Open the `.env` file and look for the `NODE_EXTRA_CA_CERTS` environment variable
|
||||
toward the top. You will need to modify its value to be the path to Homebrew's
|
||||
CA certificate file.
|
||||
|
||||
Run `brew --prefix` to see Homebrew's root directory, and use that value to
|
||||
update the `NODE_EXTRA_CA_CERTS` environment variable with the path. For
|
||||
example, if `brew --prefix` gave `/opt/homebrew` as output, then the line would
|
||||
look like this:
|
||||
|
||||
```
|
||||
NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem
|
||||
```
|
||||
|
||||
Save this change to the `.env` file and you'll be squared away.
|
||||
|
||||
## Running the Project and Routine Maintenance
|
||||
|
||||
The first time you run the project you'll need to run the project setup from the
|
||||
root project directory:
|
||||
|
||||
```sh
|
||||
make bootstrap
|
||||
```
|
||||
|
||||
This command is handled by the `Makefile` file in the root project directory, as
|
||||
are a few others.
|
||||
|
||||
_NOTE: You'll want to occasionally run `make bootstrap` to keep your project up-to-date, especially when there are dependency updates._
|
||||
|
||||
Now you can run the web server and background workers for asynchronous jobs:
|
||||
|
||||
```sh
|
||||
make run-flask
|
||||
```
|
||||
|
||||
This will run the local development web server and make the admin site
|
||||
available at http://localhost:6012; remember to make sure that the Notify.gov
|
||||
API is running as well!
|
||||
|
||||
## Git Hooks
|
||||
|
||||
We're using [`pre-commit`](https://pre-commit.com/) to manage hooks in order to
|
||||
automate common tasks or easily-missed cleanup. It's installed as part of
|
||||
`make bootstrap` and is limited to this project's virtualenv.
|
||||
|
||||
To run the hooks in advance of a `git` operation, use
|
||||
`poetry run pre-commit run`. For running across the whole codebase (useful after
|
||||
adding a new hook), use `poetry run pre-commit run --all-files`.
|
||||
|
||||
The configuration is stored in `.pre-commit-config.yaml`. In that config, there
|
||||
are links to the repos from which the hooks are pulled, so hop through there if
|
||||
ou want a detailed description of what each one is doing.
|
||||
|
||||
We do not maintain any hooks in this repository.
|
||||
|
||||
## Python Dependency Management
|
||||
|
||||
We're using [`Poetry`](https://python-poetry.org/) for managing our Python
|
||||
dependencies and local virtual environments. When it comes to managing the
|
||||
Python dependencies, there are a couple of things to bear in mind.
|
||||
|
||||
For situations where you manually manipulate the `pyproject.toml` file, you
|
||||
should use the `make py-lock` command to sync the `poetry.lock` file. This will
|
||||
ensure that you don't inadvertently bring in other transitive dependency updates
|
||||
that have not been fully tested with the project yet.
|
||||
|
||||
If you're just trying to update a dependency to a newer (or the latest) version,
|
||||
you should let Poetry take care of that for you by running the following:
|
||||
|
||||
```sh
|
||||
poetry update <dependency> [<dependency>...]
|
||||
```
|
||||
|
||||
You can specify more than one dependency together. With this command, Poetry
|
||||
will do the following for you:
|
||||
|
||||
- Find the latest compatible version(s) of the specified dependency/dependencies
|
||||
- Install the new versions
|
||||
- Update and sync the `poetry.lock` file
|
||||
|
||||
In either situation, once you are finished and have verified the dependency
|
||||
changes are working, please be sure to commit both the `pyproject.toml` and
|
||||
`poetry.lock` files.
|
||||
|
||||
### Keeping the notification-utils Dependency Up-to-Date
|
||||
|
||||
The `notifications-utils` dependency references the other repository we have at
|
||||
https://github.com/GSA/notifications-utils - this dependency requires a bit of
|
||||
extra legwork to ensure it stays up-to-date.
|
||||
|
||||
Whenever a PR is merged in the `notifications-utils` repository, we need to make
|
||||
sure the changes are pulled in here and committed to this repository as well.
|
||||
You can do this by going through these steps:
|
||||
|
||||
- Make sure your local `main` branch is up-to-date
|
||||
- Create a new branch to work in
|
||||
- Run `make update-utils`
|
||||
- Commit the updated `poetry.lock` file and push the changes
|
||||
- Make a new PR with the change
|
||||
- Have the PR get reviewed and merged
|
||||
If you are using VS Code, there are also instructions for [running inside Docker](./docs/docker-remote-containers.md)
|
||||
|
||||
### Python dependency management
|
||||
|
||||
@@ -392,7 +87,7 @@ that have not been fully tested with the project yet.
|
||||
If you're just trying to update a dependency to a newer (or the latest) version,
|
||||
you should let Poetry take care of that for you by running the following:
|
||||
|
||||
```sh
|
||||
```
|
||||
poetry update <dependency> [<dependency>...]
|
||||
```
|
||||
|
||||
@@ -424,21 +119,6 @@ You can do this by going through these steps:
|
||||
- Make a new PR with the change
|
||||
- Have the PR get reviewed and merged
|
||||
|
||||
## Known Installation Issues
|
||||
|
||||
### Python Installation Errors
|
||||
|
||||
On M1 Macs, if you get a `fatal error: 'Python.h' file not found` message, try a
|
||||
different method of installing Python. The recommended approach is to use
|
||||
[`pyenv`](https://github.com/pyenv/pyenv), as noted above in the installation
|
||||
instructions.
|
||||
|
||||
If you're using PyCharm for Python development, we've noticed some quirkiness
|
||||
with the IDE and the interaction between Poetry and virtual environment
|
||||
management that could cause a variety of problems to come up during project
|
||||
setup and dependency management. Other tools, such as Visual Studio Code, have
|
||||
proven to be a smoother experience for folks.
|
||||
|
||||
## To test the application
|
||||
|
||||
From a terminal within the running devcontainer:
|
||||
@@ -451,13 +131,11 @@ make test
|
||||
npm run test-watch
|
||||
```
|
||||
|
||||
To run a specific JavaScript test, you'll need to copy the full command from
|
||||
`package.json`.
|
||||
To run a specific JavaScript test, you'll need to copy the full command from `package.json`.
|
||||
|
||||
## Running a11y-scans locally
|
||||
|
||||
Unlike most of the tests and scans, pa11y-ci cannot currently be run from within
|
||||
the VSCode dev container.
|
||||
Unlike most of the tests and scans, pa11y-ci cannot currently be run from within the VSCode dev container.
|
||||
|
||||
1. `make run-flask` from within the devcontainer
|
||||
2. Run `make a11y-scan` from your host computer.
|
||||
@@ -470,31 +148,16 @@ the VSCode dev container.
|
||||
|
||||
## License && public domain
|
||||
|
||||
Work through
|
||||
[commit `543be77`](https://github.com/GSA/notifications-admin/commit/543be77776b64fddb6ba70fbb015ecd81a372478)
|
||||
is licensed by the UK government under the MIT license. Work after that commit
|
||||
is in the worldwide public domain. See [LICENSE.md](./LICENSE.md) for more
|
||||
information.
|
||||
Work through [commit `543be77`](https://github.com/GSA/notifications-admin/commit/543be77776b64fddb6ba70fbb015ecd81a372478) is licensed by the UK government under the MIT license. Work after that commit is in the worldwide public domain. See [LICENSE.md](./LICENSE.md) for more information.
|
||||
|
||||
## Contributing
|
||||
|
||||
As stated in [CONTRIBUTING.md](CONTRIBUTING.md), all contributions to this
|
||||
project will be released under the CC0 dedication. By submitting a pull request,
|
||||
you are agreeing to comply with this waiver of copyright interest.
|
||||
As stated in [CONTRIBUTING.md](CONTRIBUTING.md), all contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest.
|
||||
|
||||
## About the TTS Public Benefits Studio
|
||||
|
||||
The Public Benefits Studio is a team inside of
|
||||
[GSA’s Technology Transformation Services](https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services)
|
||||
(TTS), home to innovative programs like [18F](https://18f.gsa.gov/) and
|
||||
[Login.gov](https://login.gov). We collaborate with benefits programs to develop
|
||||
shared technology tools and best practices that reduce the burden of navigating
|
||||
government programs for low income individuals and families.
|
||||
The Public Benefits Studio is a team inside of [GSA’s Technology Transformation Services](https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services) (TTS), home to innovative programs like [18F](https://18f.gsa.gov/) and [Login.gov](https://login.gov). We collaborate with benefits programs to develop shared technology tools and best practices that reduce the burden of navigating government programs for low income individuals and families.
|
||||
|
||||
We’re a cross-functional team of technologists with specialized experience
|
||||
working across public benefits programs like Medicaid, SNAP, and unemployment
|
||||
insurance.
|
||||
We’re a cross-functional team of technologists with specialized experience working across public benefits programs like Medicaid, SNAP, and unemployment insurance.
|
||||
|
||||
For more information on what we're working on, the Notify tool, and how to get
|
||||
involved with our team,
|
||||
[see our flyer.](https://github.com/GSA/notifications-admin/blob/main/docs/notify-pilot-flyer.md)
|
||||
For more information on what we're working on, the Notify tool, and how to get involved with our team, [see our flyer.](https://github.com/GSA/notifications-admin/blob/main/docs/notify-pilot-flyer.md)
|
||||
|
||||
@@ -39,7 +39,6 @@ from app.asset_fingerprinter import asset_fingerprinter
|
||||
from app.config import configs
|
||||
from app.extensions import redis_client, zendesk_client
|
||||
from app.formatters import (
|
||||
convert_markdown_template,
|
||||
convert_to_boolean,
|
||||
format_auth_type,
|
||||
format_billions,
|
||||
@@ -54,7 +53,6 @@ from app.formatters import (
|
||||
format_datetime_normal,
|
||||
format_datetime_relative,
|
||||
format_datetime_short,
|
||||
format_datetime_short_america,
|
||||
format_day_of_week,
|
||||
format_delta,
|
||||
format_delta_days,
|
||||
@@ -90,12 +88,12 @@ from app.navigation import (
|
||||
HeaderNavigation,
|
||||
MainNavigation,
|
||||
OrgNavigation,
|
||||
SecondaryNavigation,
|
||||
)
|
||||
from app.notify_client import InviteTokenError
|
||||
from app.notify_client.api_key_api_client import api_key_api_client
|
||||
from app.notify_client.billing_api_client import billing_api_client
|
||||
from app.notify_client.complaint_api_client import complaint_api_client
|
||||
from app.notify_client.email_branding_client import email_branding_client
|
||||
from app.notify_client.events_api_client import events_api_client
|
||||
from app.notify_client.inbound_number_client import inbound_number_client
|
||||
from app.notify_client.invite_api_client import invite_api_client
|
||||
@@ -107,13 +105,18 @@ from app.notify_client.performance_dashboard_api_client import (
|
||||
performance_dashboard_api_client,
|
||||
)
|
||||
from app.notify_client.platform_stats_api_client import platform_stats_api_client
|
||||
from app.notify_client.provider_client import provider_client
|
||||
from app.notify_client.service_api_client import service_api_client
|
||||
from app.notify_client.status_api_client import status_api_client
|
||||
from app.notify_client.template_folder_api_client import template_folder_api_client
|
||||
from app.notify_client.template_statistics_api_client import template_statistics_client
|
||||
from app.notify_client.upload_api_client import upload_api_client
|
||||
from app.notify_client.user_api_client import user_api_client
|
||||
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
|
||||
from app.url_converters import (
|
||||
SimpleDateTypeConverter,
|
||||
TemplateTypeConverter,
|
||||
TicketTypeConverter,
|
||||
)
|
||||
|
||||
login_manager = LoginManager()
|
||||
csrf = CSRFProtect()
|
||||
@@ -131,7 +134,6 @@ navigation = {
|
||||
"main_navigation": MainNavigation(),
|
||||
"header_navigation": HeaderNavigation(),
|
||||
"org_navigation": OrgNavigation(),
|
||||
"secondary_navigation": SecondaryNavigation(),
|
||||
}
|
||||
|
||||
|
||||
@@ -148,15 +150,8 @@ def _csp(config):
|
||||
"'unsafe-eval'",
|
||||
"https://js-agent.newrelic.com",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.googletagmanager.com",
|
||||
"https://www.google-analytics.com",
|
||||
"https://dap.digitalgov.gov",
|
||||
],
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.google-analytics.com",
|
||||
],
|
||||
"connect-src": ["'self'", "https://gov-bam.nr-data.net"],
|
||||
"style-src": ["'self'", asset_domain],
|
||||
"img-src": ["'self'", asset_domain, logo_domain],
|
||||
}
|
||||
@@ -185,6 +180,7 @@ def create_app(application):
|
||||
api_key_api_client,
|
||||
billing_api_client,
|
||||
complaint_api_client,
|
||||
email_branding_client,
|
||||
events_api_client,
|
||||
inbound_number_client,
|
||||
invite_api_client,
|
||||
@@ -194,6 +190,7 @@ def create_app(application):
|
||||
organizations_client,
|
||||
performance_dashboard_api_client,
|
||||
platform_stats_api_client,
|
||||
provider_client,
|
||||
service_api_client,
|
||||
status_api_client,
|
||||
template_folder_api_client,
|
||||
@@ -287,7 +284,6 @@ def init_app(application):
|
||||
|
||||
@application.context_processor
|
||||
def _attach_current_global_daily_messages():
|
||||
global_limit = 0
|
||||
remaining_global_messages = 0
|
||||
if current_app:
|
||||
if request.view_args:
|
||||
@@ -305,10 +301,7 @@ def init_app(application):
|
||||
remaining_global_messages = global_limit - global_messages_count.get(
|
||||
"count"
|
||||
)
|
||||
return {
|
||||
"global_message_limit": global_limit,
|
||||
"daily_global_messages_remaining": remaining_global_messages,
|
||||
}
|
||||
return {"daily_global_messages_remaining": remaining_global_messages}
|
||||
|
||||
@application.before_request
|
||||
def record_start_time():
|
||||
@@ -326,6 +319,7 @@ def init_app(application):
|
||||
|
||||
application.url_map.converters["uuid"].to_python = lambda self, value: value
|
||||
application.url_map.converters["template_type"] = TemplateTypeConverter
|
||||
application.url_map.converters["ticket_type"] = TicketTypeConverter
|
||||
application.url_map.converters["simple_date"] = SimpleDateTypeConverter
|
||||
|
||||
|
||||
@@ -549,7 +543,6 @@ def add_template_filters(application):
|
||||
format_datetime_24h,
|
||||
format_datetime_normal,
|
||||
format_datetime_short,
|
||||
format_datetime_short_america,
|
||||
valid_phone_number,
|
||||
linkable_name,
|
||||
format_date,
|
||||
@@ -587,7 +580,6 @@ def add_template_filters(application):
|
||||
format_mobile_network,
|
||||
format_yes_no,
|
||||
square_metres_to_square_miles,
|
||||
convert_markdown_template,
|
||||
]:
|
||||
application.add_template_filter(fn)
|
||||
|
||||
|
||||
15
app/assets/javascripts/consent.js
Normal file
15
app/assets/javascripts/consent.js
Normal file
@@ -0,0 +1,15 @@
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
function hasConsentFor (cookieCategory, consentCookie) {
|
||||
if (consentCookie === undefined) { consentCookie = window.GOVUK.getConsentCookie(); }
|
||||
|
||||
if (consentCookie === null) { return false; }
|
||||
|
||||
if (!(cookieCategory in consentCookie)) { return false; }
|
||||
|
||||
return consentCookie[cookieCategory];
|
||||
}
|
||||
|
||||
window.GOVUK.hasConsentFor = hasConsentFor;
|
||||
})(window);
|
||||
104
app/assets/javascripts/cookieMessage.js
Normal file
104
app/assets/javascripts/cookieMessage.js
Normal file
@@ -0,0 +1,104 @@
|
||||
window.GOVUK = window.GOVUK || {};
|
||||
window.GOVUK.Modules = window.GOVUK.Modules || {};
|
||||
|
||||
(function (Modules) {
|
||||
function CookieBanner () { }
|
||||
|
||||
CookieBanner.clearOldCookies = function (consent) {
|
||||
var gaCookies = ['_ga', '_gid'];
|
||||
|
||||
// clear old cookie set by our previous JS, set on the www domain
|
||||
if (window.GOVUK.cookie('seen_cookie_message')) {
|
||||
document.cookie = 'seen_cookie_message=;expires=' + new Date().toGMTString() + ';path=/';
|
||||
}
|
||||
|
||||
if (consent === null) {
|
||||
for (var i = 0; i < gaCookies.length; i++) {
|
||||
if (window.GOVUK.cookie(gaCookies[i])) {
|
||||
// GA cookies are set on the base domain so need the www stripping
|
||||
var cookieString = gaCookies[i] + '=;expires=' + new Date().toGMTString() + ';domain=' + window.location.hostname.replace(/^www\./, '.') + ';path=/';
|
||||
document.cookie = cookieString;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.start = function ($module) {
|
||||
this.$module = $module[0];
|
||||
this.$module.hideCookieMessage = this.hideCookieMessage.bind(this);
|
||||
this.$module.showConfirmationMessage = this.showConfirmationMessage.bind(this);
|
||||
this.$module.setCookieConsent = this.setCookieConsent.bind(this);
|
||||
|
||||
this.$module.cookieBanner = document.querySelector('.notify-cookie-banner');
|
||||
this.$module.cookieBannerConfirmationMessage = this.$module.querySelector('.notify-cookie-banner__confirmation');
|
||||
|
||||
this.setupCookieMessage();
|
||||
};
|
||||
|
||||
CookieBanner.prototype.setupCookieMessage = function () {
|
||||
this.$hideLink = this.$module.querySelector('button[data-hide-cookie-banner]');
|
||||
if (this.$hideLink) {
|
||||
this.$hideLink.addEventListener('click', this.$module.hideCookieMessage);
|
||||
}
|
||||
|
||||
this.$acceptCookiesLink = this.$module.querySelector('button[data-accept-cookies=true]');
|
||||
if (this.$acceptCookiesLink) {
|
||||
this.$acceptCookiesLink.addEventListener('click', () => this.$module.setCookieConsent(true));
|
||||
}
|
||||
|
||||
this.$rejectCookiesLink = this.$module.querySelector('button[data-accept-cookies=false]');
|
||||
if (this.$rejectCookiesLink) {
|
||||
this.$rejectCookiesLink.addEventListener('click', () => this.$module.setCookieConsent(false));
|
||||
}
|
||||
|
||||
this.showCookieMessage();
|
||||
};
|
||||
|
||||
CookieBanner.prototype.showCookieMessage = function () {
|
||||
// Show the cookie banner if not in the cookie settings page
|
||||
if (!this.isInCookiesPage()) {
|
||||
var hasCookiesPolicy = window.GOVUK.cookie('cookies_policy');
|
||||
|
||||
if (this.$module && !hasCookiesPolicy) {
|
||||
this.$module.style.display = 'block';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.hideCookieMessage = function (event) {
|
||||
if (this.$module) {
|
||||
this.$module.style.display = 'none';
|
||||
}
|
||||
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.setCookieConsent = function (analyticsConsent) {
|
||||
window.GOVUK.setConsentCookie({ 'analytics': analyticsConsent });
|
||||
|
||||
this.$module.showConfirmationMessage(analyticsConsent);
|
||||
this.$module.cookieBannerConfirmationMessage.focus();
|
||||
|
||||
if (analyticsConsent) { window.GOVUK.initAnalytics(); }
|
||||
};
|
||||
|
||||
CookieBanner.prototype.showConfirmationMessage = function (analyticsConsent) {
|
||||
var messagePrefix = analyticsConsent ? 'You’ve accepted analytics cookies.' : 'You told us not to use analytics cookies.';
|
||||
|
||||
this.$cookieBannerMainContent = document.querySelector('.notify-cookie-banner__wrapper');
|
||||
this.$cookieBannerConfirmationMessage = document.querySelector('.notify-cookie-banner__confirmation-message');
|
||||
|
||||
this.$cookieBannerConfirmationMessage.insertAdjacentText('afterbegin', messagePrefix);
|
||||
this.$cookieBannerMainContent.style.display = 'none';
|
||||
this.$module.cookieBannerConfirmationMessage.style.display = 'block';
|
||||
};
|
||||
|
||||
CookieBanner.prototype.isInCookiesPage = function () {
|
||||
return window.location.pathname === '/cookies';
|
||||
};
|
||||
|
||||
Modules.CookieBanner = CookieBanner;
|
||||
})(window.GOVUK.Modules);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
// Show the current year
|
||||
const currentYearElement = document.getElementById("current-year");
|
||||
|
||||
if (currentYearElement) {
|
||||
currentYearElement.textContent = new Date().getFullYear();
|
||||
}
|
||||
})(window);
|
||||
163
app/assets/javascripts/govuk/cookie-functions.js
Normal file
163
app/assets/javascripts/govuk/cookie-functions.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// used by the cookie banner component
|
||||
|
||||
(function (root) {
|
||||
'use strict';
|
||||
window.GOVUK = window.GOVUK || {};
|
||||
|
||||
var DEFAULT_COOKIE_CONSENT = {
|
||||
'analytics': false
|
||||
};
|
||||
|
||||
var COOKIE_CATEGORIES = {
|
||||
'_ga': 'analytics',
|
||||
'_gid': 'analytics'
|
||||
};
|
||||
|
||||
/*
|
||||
Cookie methods
|
||||
==============
|
||||
|
||||
Usage:
|
||||
|
||||
Setting a cookie:
|
||||
GOVUK.cookie('hobnob', 'tasty', { days: 30 });
|
||||
|
||||
Reading a cookie:
|
||||
GOVUK.cookie('hobnob');
|
||||
|
||||
Deleting a cookie:
|
||||
GOVUK.cookie('hobnob', null);
|
||||
*/
|
||||
window.GOVUK.cookie = function (name, value, options) {
|
||||
if (typeof value !== 'undefined') {
|
||||
if (value === false || value === null) {
|
||||
return window.GOVUK.setCookie(name, '', { days: -1 });
|
||||
} else {
|
||||
// Default expiry date of 30 days
|
||||
if (typeof options === 'undefined') {
|
||||
options = { days: 30 };
|
||||
}
|
||||
return window.GOVUK.setCookie(name, value, options);
|
||||
}
|
||||
} else {
|
||||
return window.GOVUK.getCookie(name);
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.getConsentCookie = function () {
|
||||
var consentCookie = window.GOVUK.cookie('cookies_policy');
|
||||
var consentCookieObj;
|
||||
|
||||
if (consentCookie) {
|
||||
try {
|
||||
consentCookieObj = JSON.parse(consentCookie);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof consentCookieObj !== 'object' && consentCookieObj !== null) {
|
||||
consentCookieObj = JSON.parse(consentCookieObj);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return consentCookieObj;
|
||||
};
|
||||
|
||||
window.GOVUK.setConsentCookie = function (options) {
|
||||
var cookieConsent = window.GOVUK.getConsentCookie();
|
||||
|
||||
if (!cookieConsent) {
|
||||
cookieConsent = JSON.parse(JSON.stringify(DEFAULT_COOKIE_CONSENT));
|
||||
}
|
||||
|
||||
for (var cookieType in options) {
|
||||
cookieConsent[cookieType] = options[cookieType];
|
||||
|
||||
// Delete cookies of that type if consent being set to false
|
||||
if (!options[cookieType]) {
|
||||
for (var cookie in COOKIE_CATEGORIES) {
|
||||
if (COOKIE_CATEGORIES[cookie] === cookieType) {
|
||||
window.GOVUK.cookie(cookie, null);
|
||||
|
||||
if (window.GOVUK.cookie(cookie)) {
|
||||
document.cookie = cookie + '=;expires=' + new Date() + ';domain=' + window.location.hostname.replace(/^www\./, '.') + ';path=/';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.GOVUK.setCookie('cookies_policy', JSON.stringify(cookieConsent), { days: 365 });
|
||||
};
|
||||
|
||||
window.GOVUK.checkConsentCookieCategory = function (cookieName, cookieCategory) {
|
||||
var currentConsentCookie = window.GOVUK.getConsentCookie();
|
||||
|
||||
// If the consent cookie doesn't exist, but the cookie is in our known list, return true
|
||||
if (!currentConsentCookie && COOKIE_CATEGORIES[cookieName]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentConsentCookie = window.GOVUK.getConsentCookie();
|
||||
|
||||
// Sometimes currentConsentCookie is malformed in some of the tests, so we need to handle these
|
||||
try {
|
||||
return currentConsentCookie[cookieCategory];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.checkConsentCookie = function (cookieName, cookieValue) {
|
||||
// If we're setting the consent cookie OR deleting a cookie, allow by default
|
||||
if (cookieName === 'cookies_policy' || (cookieValue === null || cookieValue === false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (COOKIE_CATEGORIES[cookieName]) {
|
||||
var cookieCategory = COOKIE_CATEGORIES[cookieName];
|
||||
|
||||
return window.GOVUK.checkConsentCookieCategory(cookieName, cookieCategory);
|
||||
} else {
|
||||
// Deny the cookie if it is not known to us
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.setCookie = function (name, value, options) {
|
||||
if (window.GOVUK.checkConsentCookie(name, value)) {
|
||||
if (typeof options === 'undefined') {
|
||||
options = {};
|
||||
}
|
||||
var cookieString = name + '=' + value + '; path=/';
|
||||
if (options.days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (options.days * 24 * 60 * 60 * 1000));
|
||||
cookieString = cookieString + '; expires=' + date.toGMTString();
|
||||
}
|
||||
if (document.location.protocol === 'https:') {
|
||||
cookieString = cookieString + '; Secure';
|
||||
}
|
||||
document.cookie = cookieString;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.getCookie = function (name) {
|
||||
var nameEQ = name + '=';
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0, len = cookies.length; i < len; i++) {
|
||||
var cookie = cookies[i];
|
||||
while (cookie.charAt(0) === ' ') {
|
||||
cookie = cookie.substring(1, cookie.length);
|
||||
}
|
||||
if (cookie.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(cookie.substring(nameEQ.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}(window));
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
window.GOVUK.Frontend.initAll();
|
||||
|
||||
var consentData = window.GOVUK.getConsentCookie();
|
||||
window.GOVUK.Modules.CookieBanner.clearOldCookies(consentData);
|
||||
|
||||
if (window.GOVUK.hasConsentFor('analytics', consentData)) {
|
||||
window.GOVUK.initAnalytics();
|
||||
}
|
||||
|
||||
$(() => $("time.timeago").timeago());
|
||||
|
||||
var showHideContent = new GOVUK.ShowHideContent();
|
||||
|
||||
36
app/assets/javascripts/previewPane.js
Normal file
36
app/assets/javascripts/previewPane.js
Normal file
@@ -0,0 +1,36 @@
|
||||
(function (global) {
|
||||
|
||||
'use strict';
|
||||
|
||||
$ = global.jQuery;
|
||||
|
||||
let branding_style = $('.usa-radio input[name="branding_style"]:checked');
|
||||
|
||||
if (!branding_style.length) { return; }
|
||||
|
||||
branding_style = branding_style.val();
|
||||
|
||||
const $paneWrapper = $('<div class="govuk-grid-column-full"></div>');
|
||||
const $form = $('form');
|
||||
const previewType = $form.data('previewType');
|
||||
const $previewPane = $(`<iframe src="/_${previewType}?${buildQueryString(['branding_style', branding_style])}" class="branding-preview" scrolling="no"></iframe>`);
|
||||
|
||||
function buildQueryString () {
|
||||
return $.map(arguments, (val, idx) => encodeURI(val[0]) + '=' + encodeURI(val[1])).join('&');
|
||||
}
|
||||
|
||||
function setPreviewPane (e) {
|
||||
const $target = $(e.target);
|
||||
if ($target.attr('name') == 'branding_style') {
|
||||
branding_style = $target.val();
|
||||
}
|
||||
$previewPane.attr('src', `/_${previewType}?${buildQueryString(['branding_style', branding_style])}`);
|
||||
}
|
||||
|
||||
$paneWrapper.append($previewPane);
|
||||
$form.find('.govuk-grid-row').eq(0).prepend($paneWrapper);
|
||||
$form.attr('action', location.pathname.replace(new RegExp(`set-${previewType}-branding$`), `preview-${previewType}-branding`));
|
||||
$form.find('button[type="submit"]').text('Save');
|
||||
|
||||
$('fieldset').on('change', 'input[name="branding_style"]', setPreviewPane);
|
||||
})(window);
|
||||
@@ -22,26 +22,16 @@ i.e.
|
||||
|
||||
@use "uswds-core" as *;
|
||||
|
||||
.usa-header--extended {
|
||||
.usa-logo {
|
||||
font-family: family("sans");
|
||||
margin: units(4) 0;
|
||||
@include at-media-max('mobile-lg') {
|
||||
margin: units(4) 0 units(4) units(2);
|
||||
}
|
||||
img {
|
||||
@include at-media($theme-header-min-width) {
|
||||
width: 80px;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
.usa-header--extended .usa-logo {
|
||||
font-family: family("sans");
|
||||
margin: units(4) 0;
|
||||
@include at-media-max('mobile-lg') {
|
||||
margin: units(4) 0 units(4) units(2);
|
||||
}
|
||||
.usa-nav__secondary {
|
||||
.usa-nav__link {
|
||||
padding: 0;
|
||||
&.usa-current {
|
||||
text-decoration: underline;
|
||||
}
|
||||
img {
|
||||
@include at-media($theme-header-min-width) {
|
||||
width: 80px;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,12 +44,15 @@ i.e.
|
||||
&> .usa-nav__primary-item > a {
|
||||
font-size: size("body", 4);
|
||||
}
|
||||
// &> .usa-nav__primary-item:last-child {
|
||||
// margin-left: auto;
|
||||
// @include u-margin-right(-4);
|
||||
// }
|
||||
&> .usa-nav__primary-item:last-child {
|
||||
margin-left: auto;
|
||||
@include u-margin-right(-4);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-nav__primary
|
||||
|
||||
|
||||
h1 {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
@@ -121,48 +114,12 @@ td.table-empty-message {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.button-flex-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: units(2);
|
||||
@include at-media(desktop) {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.user-list-edit-link:active:before,
|
||||
.user-list-edit-link:focus:before {
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: units(4);
|
||||
.user-list-item {
|
||||
@include at-media(desktop) {
|
||||
flex: 1 1 calc(50% - units(2));
|
||||
flex-grow: 0;
|
||||
}
|
||||
flex: 1 1 calc(100% - units(2));
|
||||
border: 1px solid color('gray-cool-10');
|
||||
padding: units(2);
|
||||
.tick-cross-list-permissions {
|
||||
margin: units(1) 0;
|
||||
padding-left: units(2);
|
||||
}
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: size("body", "sm");
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.template-list-item-without-ancestors .template-list-folder:active,
|
||||
.template-list-item-without-ancestors .template-list-folder:active::before,
|
||||
.template-list-item-without-ancestors .template-list-folder:focus,
|
||||
@@ -175,15 +132,11 @@ td.table-empty-message {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.template-list-item-hidden-by-default {
|
||||
display: none
|
||||
}
|
||||
|
||||
.folder-heading a.folder-heading-folder,
|
||||
.template-list-folder {
|
||||
background: url(../img/material-icons/folder.svg) no-repeat;
|
||||
padding-left: units(4);
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
background-position: 0;
|
||||
}
|
||||
|
||||
@@ -213,7 +166,7 @@ td.table-empty-message {
|
||||
.template-list-template {
|
||||
background: url(../img/material-icons/description.svg) no-repeat;
|
||||
padding-left: units(4);
|
||||
display: inline-flex;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.js-enabled .live-search {
|
||||
@@ -261,7 +214,7 @@ td.table-empty-message {
|
||||
}
|
||||
|
||||
.usa-hero {
|
||||
background-image: none;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.usa-prose {
|
||||
@@ -279,7 +232,7 @@ td.table-empty-message {
|
||||
}
|
||||
|
||||
.navigation-service.usa-breadcrumb {
|
||||
bottom: 0;
|
||||
-bottom: 0;
|
||||
}
|
||||
|
||||
// Dashboard
|
||||
@@ -293,13 +246,13 @@ td.table-empty-message {
|
||||
text-decoration: none;
|
||||
&:hover{
|
||||
background: color("blue-warm-70v");
|
||||
}
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: white;
|
||||
color: white;
|
||||
}
|
||||
.big-number-smaller {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.big-number-number {
|
||||
font-size: units(5);
|
||||
@@ -311,15 +264,15 @@ td.table-empty-message {
|
||||
}
|
||||
.big-number-status {
|
||||
background: color("green-cool-40v");
|
||||
display: flex;
|
||||
display: flex;
|
||||
padding: units(1) units(2);
|
||||
&--failing {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
a.usa-link {
|
||||
color: white;
|
||||
background: color("red-warm-50v");
|
||||
padding: units(1) units(2);
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background: color("red-warm-60v");
|
||||
@@ -329,12 +282,12 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
.usa-table {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
caption {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table-field-center-aligned {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
.template-statistics-table-template-name {
|
||||
padding-left: units(4);
|
||||
@@ -347,89 +300,63 @@ td.table-empty-message {
|
||||
|
||||
.dashboard-table {
|
||||
table {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.file-list-filename {
|
||||
font-weight: bold;
|
||||
}
|
||||
.file-list-hint {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.table-field, .table-field-right-aligned {
|
||||
width: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
&.usage-table {
|
||||
.table-field, .table-field-left-aligned, .table-field-right-aligned {
|
||||
width: auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usage-table {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.big-number-smallest {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.job-table {
|
||||
.usa-table {
|
||||
thead th.file-name {
|
||||
width: 25%;
|
||||
}
|
||||
thead th.template {
|
||||
width: 20%;
|
||||
}
|
||||
thead th.time-sent {
|
||||
width: 30%;
|
||||
}
|
||||
thead th.sender {
|
||||
width: 15%;
|
||||
}
|
||||
thead th.\#-of-recipients {
|
||||
width: 5%;
|
||||
}
|
||||
thead th.report {
|
||||
width: 5%;
|
||||
}
|
||||
th {
|
||||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#template-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: units(1) 0 units(1) units(1);
|
||||
margin: units(2) 0 units(5);
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs
|
||||
|
||||
.tabs {
|
||||
.pill {
|
||||
display: flex;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
.pill-item__container {
|
||||
border: 1px solid color("gray-cool-10");
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
font-size: units(2);
|
||||
a {
|
||||
a {
|
||||
padding: units(4);
|
||||
.big-number-smaller {
|
||||
font-size: units(5);
|
||||
@@ -440,7 +367,7 @@ td.table-empty-message {
|
||||
}
|
||||
&:not(.pill-item--selected):hover {
|
||||
background: color("blue-warm-70v");
|
||||
}
|
||||
}
|
||||
&.pill-item--selected:hover {
|
||||
color: color("blue-60v");
|
||||
}
|
||||
@@ -452,12 +379,12 @@ td.table-empty-message {
|
||||
// Etc
|
||||
|
||||
.email-brand, .browse-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
margin-bottom: units(2);
|
||||
li {
|
||||
padding: 8px 0;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +392,7 @@ details form {
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
// Textbox highlight
|
||||
// Textbox highlight
|
||||
|
||||
.textbox-highlight {
|
||||
|
||||
@@ -513,7 +440,7 @@ details form {
|
||||
.placeholder,
|
||||
.placeholder-conditional {
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
position: relative;
|
||||
&:after {
|
||||
content: "";
|
||||
background-color: color("yellow-20v");
|
||||
@@ -522,9 +449,9 @@ details form {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
right: 6px;
|
||||
right: 6px;
|
||||
height: 100%;
|
||||
border-radius: 7px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +471,3 @@ details form {
|
||||
padding-left: 5px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.edit-textbox-error-mt {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Get started
|
||||
|
||||
|
||||
1. ## Check if Notify.gov is right for you
|
||||
<p>Read about our <a class="usa-link" href="{{ url_for('main.features') }}">features</a>, <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a> and <a class="usa-link" href="{{ url_for('main.roadmap') }}">roadmap</a>.</p>
|
||||
|
||||
|
||||
2. ## Create an account
|
||||
{% if not current_user.is_authenticated %}
|
||||
<p><a class="usa-link" href="{{ url_for('.register') }}">Create an account</a> for free and add your first Notify service. When you add a new service it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% else %}
|
||||
<p>Create an account for free and add your first Notify service. When you add a new service, it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% endif %}
|
||||
|
||||
3. ## Write some messages
|
||||
{% if True %}
|
||||
<p>Add message templates with examples of the content you plan to send. You can use our <a class="usa-link" href="{{ url_for('main.guidance_index') }}">guidance</a> to help you.</p>
|
||||
{% endif %}
|
||||
|
||||
4. ## Set up your service
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>Review your settings to add message branding and sender information.</p>
|
||||
<p>Add team members and check their permissions.</p>
|
||||
{% else %}
|
||||
<p>Review your <a class="usa-link" href="{{ url_for('.service_settings', service_id=current_service.id) }}">settings</a> to add message branding and sender information.</p>
|
||||
<p>Add <a class="usa-link" href="{{ url_for('.manage_users', service_id=current_service.id) }}">team members</a> and check their permissions.</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
5. ## Start sending messages
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>When you’re ready to send messages to people outside your team, go to the <b class="bold">Settings</b> page and select <b class="bold">Request to go live</b>. We’ll approve your request within one working day.</p>
|
||||
{% else %}
|
||||
<p>You should <a class="usa-link" href="{{ url_for('.support') }}">request to go live</a> when you’re ready to send messages to people outside your team. We’ll approve your request within one working day.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- <li class="get-started-list__item">
|
||||
<h2 class="usa-process-list__heading line-height-sans-1">Set up an API integration (optional)</h2>
|
||||
<p>You can use the Notify API to send messages automatically.</p>
|
||||
<p>Our <a class="usa-link" href="{{ url_for('main.documentation') }}">documentation</a> explains how to integrate the API with a web application or back office system.</p>
|
||||
</li> -->
|
||||
|
||||
<!-- <p>Check <a class="usa-link" href="{{ url_for('main.how_to_pay') }}">how to pay</a> if you’re planning to exceed the <a class="usa-link" href="{{ url_for('.pricing', _anchor='text-messages') }}">free text message allowance</a>.</p> -->
|
||||
@@ -24,7 +24,6 @@ EVENT_SCHEMAS = {
|
||||
"service_id",
|
||||
"ui_permissions",
|
||||
},
|
||||
"resend_user_invite_to_service": {"email_address", "resent_by_id", "service_id"},
|
||||
"cancel_user_invite_to_service": {"email_address", "canceled_by_id", "service_id"},
|
||||
"set_user_permissions": {
|
||||
"user_id",
|
||||
@@ -64,10 +63,6 @@ def create_cancel_user_invite_to_service_event(**kwargs):
|
||||
_send_event("cancel_user_invite_to_service", **kwargs)
|
||||
|
||||
|
||||
def create_resend_user_invite_to_service_event(**kwargs):
|
||||
_send_event("resend_user_invite_to_service", **kwargs)
|
||||
|
||||
|
||||
def create_add_user_to_service_event(**kwargs):
|
||||
_send_event("add_user_to_service", **kwargs)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
import urllib
|
||||
@@ -10,56 +9,17 @@ from numbers import Number
|
||||
import ago
|
||||
import dateutil
|
||||
import humanize
|
||||
import markdown
|
||||
import pytz
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import Markup, render_template_string, url_for
|
||||
from flask.helpers import get_root_path
|
||||
from flask import Markup, url_for
|
||||
from notifications_utils.field import Field
|
||||
from notifications_utils.formatters import make_quotes_smart
|
||||
from notifications_utils.formatters import nl2br as utils_nl2br
|
||||
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
|
||||
from notifications_utils.take import Take
|
||||
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
from app.utils.time import parse_naive_dt
|
||||
|
||||
|
||||
def apply_html_class(tags, html_file):
|
||||
new_html = html_file
|
||||
|
||||
for tag in tags:
|
||||
element = tag[0]
|
||||
class_name = tag[1]
|
||||
|
||||
soup = BeautifulSoup(new_html, "html.parser")
|
||||
|
||||
for xtag in soup.find_all(element):
|
||||
xtag["class"] = class_name
|
||||
|
||||
new_html = str(soup)
|
||||
|
||||
return new_html
|
||||
|
||||
|
||||
def convert_markdown_template(mdf, test=False):
|
||||
content_text = ""
|
||||
|
||||
if not test:
|
||||
APP_ROOT = get_root_path("notifications-admin")
|
||||
file = "app/content/" + mdf + ".md"
|
||||
md_file = os.path.join(APP_ROOT, file)
|
||||
with open(md_file) as f:
|
||||
content_text = f.read()
|
||||
else:
|
||||
content_text = mdf
|
||||
|
||||
jn_render = render_template_string(content_text)
|
||||
md_render = markdown.markdown(jn_render)
|
||||
|
||||
return md_render
|
||||
|
||||
|
||||
def convert_to_boolean(value):
|
||||
if isinstance(value, str):
|
||||
if value.lower() in ["t", "true", "on", "yes", "1"]:
|
||||
@@ -71,14 +31,13 @@ def convert_to_boolean(value):
|
||||
|
||||
|
||||
def format_datetime(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
return "{} at {} UTC".format(format_date(date), format_time_24h(date))
|
||||
|
||||
|
||||
def format_datetime_24h(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
return "{} at {} UTC".format(
|
||||
format_date(date),
|
||||
format_time_24h(date),
|
||||
)
|
||||
|
||||
|
||||
@@ -87,74 +46,39 @@ def format_time(date):
|
||||
|
||||
|
||||
def format_datetime_normal(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date_normal(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
return "{} at {} UTC".format(format_date_normal(date), format_time_24h(date))
|
||||
|
||||
|
||||
def format_datetime_short(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date_short(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
|
||||
|
||||
def format_datetime_short_america(date):
|
||||
return "{} at {}".format(format_date_numeric_america(date), format_time_12h(date))
|
||||
|
||||
|
||||
def format_date_numeric_america(date):
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
|
||||
)
|
||||
|
||||
|
||||
def format_time_12h(date):
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
|
||||
)
|
||||
return "{} at {} UTC".format(format_date_short(date), format_time_24h(date))
|
||||
|
||||
|
||||
def format_datetime_relative(date):
|
||||
return "{} at {} {}".format(
|
||||
get_human_day(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
return "{} at {} UTC".format(get_human_day(date), format_time_24h(date))
|
||||
|
||||
|
||||
def format_datetime_numeric(date):
|
||||
return "{} {} {}".format(
|
||||
format_date_numeric(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
return "{} {} UTC".format(
|
||||
format_date_numeric(date),
|
||||
format_time_24h(date),
|
||||
)
|
||||
|
||||
|
||||
def format_date_numeric(date):
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%Y-%m-%d")
|
||||
)
|
||||
return date.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def format_time_24h(date):
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%H:%M")
|
||||
return date.strftime("%H:%M")
|
||||
|
||||
|
||||
def get_human_day(time, date_prefix=""):
|
||||
# Add 1 minute to transform 00:00 into ‘midnight today’ instead of ‘midnight tomorrow’
|
||||
time = parse_naive_dt(time)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
time = time.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
|
||||
date = (time - timedelta(minutes=1)).date()
|
||||
now = datetime.now(preferred_tz)
|
||||
now = datetime.now(pytz.utc)
|
||||
|
||||
if date == (now + timedelta(days=1)).date():
|
||||
return "tomorrow"
|
||||
@@ -176,12 +100,7 @@ def get_human_day(time, date_prefix=""):
|
||||
|
||||
def format_date(date):
|
||||
date = parse_naive_dt(date)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc)
|
||||
.astimezone(preferred_tz)
|
||||
.strftime("%A %d %B %Y")
|
||||
)
|
||||
return date.strftime("%A %d %B %Y")
|
||||
|
||||
|
||||
def format_date_normal(date):
|
||||
@@ -191,10 +110,7 @@ def format_date_normal(date):
|
||||
|
||||
def format_date_short(date):
|
||||
date = parse_naive_dt(date)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return _format_datetime_short(
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
|
||||
)
|
||||
return _format_datetime_short(date)
|
||||
|
||||
|
||||
def format_date_human(date):
|
||||
@@ -202,17 +118,15 @@ def format_date_human(date):
|
||||
|
||||
|
||||
def format_datetime_human(date, date_prefix=""):
|
||||
return "{} at {} {}".format(
|
||||
return "{} at {} UTC".format(
|
||||
get_human_day(date, date_prefix="on"),
|
||||
format_time_24h(date),
|
||||
get_user_preferred_timezone(),
|
||||
)
|
||||
|
||||
|
||||
def format_day_of_week(date):
|
||||
date = parse_naive_dt(date)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%A")
|
||||
return date.strftime("%A")
|
||||
|
||||
|
||||
def _format_datetime_short(datetime):
|
||||
@@ -470,12 +384,6 @@ def message_count_noun(count, template_type):
|
||||
else:
|
||||
return "text messages"
|
||||
|
||||
if template_type == "parts":
|
||||
if count == 1:
|
||||
return "text message part"
|
||||
else:
|
||||
return "text message parts"
|
||||
|
||||
elif template_type == "email":
|
||||
if count == 1:
|
||||
return "email"
|
||||
|
||||
@@ -9,6 +9,7 @@ from app.main.views import ( # noqa isort:skip
|
||||
code_not_received,
|
||||
conversation,
|
||||
dashboard,
|
||||
email_branding,
|
||||
feedback,
|
||||
find_services,
|
||||
find_users,
|
||||
@@ -25,6 +26,7 @@ from app.main.views import ( # noqa isort:skip
|
||||
performance,
|
||||
platform_admin,
|
||||
pricing,
|
||||
providers,
|
||||
register,
|
||||
security_policy,
|
||||
send,
|
||||
|
||||
@@ -61,20 +61,18 @@ from app.main.validators import (
|
||||
ValidEmail,
|
||||
ValidGovEmail,
|
||||
)
|
||||
from app.models.feedback import PROBLEM_TICKET_TYPE, QUESTION_TICKET_TYPE
|
||||
from app.models.organization import Organization
|
||||
from app.utils import merge_jsonlike
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
from app.utils import branding, merge_jsonlike
|
||||
from app.utils.user_permissions import all_ui_permissions, permission_options
|
||||
|
||||
|
||||
def get_time_value_and_label(future_time):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
future_time.astimezone(preferred_tz).replace(tzinfo=None).isoformat(),
|
||||
"{} at {} {}".format(
|
||||
get_human_day(future_time.astimezone(preferred_tz)),
|
||||
get_human_time(future_time.astimezone(preferred_tz)),
|
||||
get_user_preferred_timezone(),
|
||||
future_time.astimezone(pytz.utc).replace(tzinfo=None).isoformat(),
|
||||
"{} at {} UTC".format(
|
||||
get_human_day(future_time.astimezone(pytz.utc)),
|
||||
get_human_time(future_time.astimezone(pytz.utc)),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -87,25 +85,21 @@ def get_human_time(time):
|
||||
|
||||
def get_human_day(time, prefix_today_with="T"):
|
||||
# Add 1 hour to get ‘midnight today’ instead of ‘midnight tomorrow’
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
time = (time - timedelta(hours=1)).strftime("%A")
|
||||
if time == datetime.now(preferred_tz).strftime("%A"):
|
||||
if time == datetime.now(pytz.utc).strftime("%A"):
|
||||
return "{}oday".format(prefix_today_with)
|
||||
if time == (datetime.now(preferred_tz) + timedelta(days=1)).strftime("%A"):
|
||||
if time == (datetime.now(pytz.utc) + timedelta(days=1)).strftime("%A"):
|
||||
return "Tomorrow"
|
||||
return time
|
||||
|
||||
|
||||
def get_furthest_possible_scheduled_time():
|
||||
# We want local time to find date boundaries
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (datetime.now(preferred_tz) + timedelta(days=4)).replace(hour=0)
|
||||
return (datetime.now(pytz.utc) + timedelta(days=4)).replace(hour=0)
|
||||
|
||||
|
||||
def get_next_hours_until(until):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
now = datetime.now(preferred_tz)
|
||||
now = datetime.now(pytz.utc)
|
||||
hours = int((until - now).total_seconds() / (60 * 60))
|
||||
return [
|
||||
(now + timedelta(hours=i)).replace(minute=0, second=0, microsecond=0)
|
||||
@@ -114,8 +108,7 @@ def get_next_hours_until(until):
|
||||
|
||||
|
||||
def get_next_days_until(until):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
now = datetime.now(preferred_tz)
|
||||
now = datetime.now(pytz.utc)
|
||||
days = int((until - now).total_seconds() / (60 * 60 * 24))
|
||||
return [
|
||||
get_human_day((now + timedelta(days=i)), prefix_today_with="Later t")
|
||||
@@ -814,6 +807,49 @@ class GovukCheckboxField(BooleanField):
|
||||
)
|
||||
|
||||
|
||||
class GovukTextareaField(TextAreaField):
|
||||
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
|
||||
super(TextAreaField, self).__init__(label, validators, **kwargs)
|
||||
self.param_extensions = param_extensions
|
||||
|
||||
# self.__call__ renders the HTML for the field by:
|
||||
# 1. delegating to self.meta.render_field which
|
||||
# 2. calls field.widget
|
||||
# this bypasses that by making self.widget a method with the same interface as widget.__call__
|
||||
def widget(self, field, param_extensions=None, **kwargs):
|
||||
# error messages
|
||||
error_message = None
|
||||
if field.errors:
|
||||
error_message = {"text": field.errors[0]}
|
||||
|
||||
params = {
|
||||
"name": field.name,
|
||||
"id": field.id,
|
||||
"rows": 8,
|
||||
"label": {
|
||||
"text": field.label.text,
|
||||
"classes": None,
|
||||
"isPageHeading": False,
|
||||
},
|
||||
"hint": {"text": None},
|
||||
"errorMessage": error_message,
|
||||
}
|
||||
|
||||
# extend default params with any sent in during instantiation
|
||||
if self.param_extensions:
|
||||
merge_jsonlike(params, self.param_extensions)
|
||||
|
||||
# add any sent in though use in templates
|
||||
if param_extensions:
|
||||
merge_jsonlike(params, param_extensions)
|
||||
|
||||
return Markup(
|
||||
render_template(
|
||||
"components/components/textarea/template.njk", params=params
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
|
||||
class GovukCheckboxesField(SelectMultipleField):
|
||||
render_as_list = False
|
||||
@@ -912,9 +948,9 @@ class OnOffField(GovukRadiosField):
|
||||
]
|
||||
super().__init__(
|
||||
label,
|
||||
*args,
|
||||
choices=choices,
|
||||
thing=f"{choices[0][1].lower()} or {choices[1][1].lower()}",
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -1152,7 +1188,7 @@ class AdminServiceSMSAllowanceForm(StripWhitespaceForm):
|
||||
|
||||
class AdminServiceMessageLimitForm(StripWhitespaceForm):
|
||||
message_limit = GovukIntegerField(
|
||||
"Max number of messages the service has per send",
|
||||
"Number of messages the service is allowed to send each day",
|
||||
validators=[DataRequired(message="Cannot be empty")],
|
||||
)
|
||||
|
||||
@@ -1235,28 +1271,6 @@ class ChangeNameForm(StripWhitespaceForm):
|
||||
new_name = GovukTextInputField("Your name")
|
||||
|
||||
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
class ChangePreferredTimezoneForm(StripWhitespaceForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChangePreferredTimezoneForm, self).__init__(*args, **kwargs)
|
||||
self.new_preferred_timezone.choices = [
|
||||
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||
("US/Eastern", "US/Eastern"),
|
||||
("US/Central", "US/Central"),
|
||||
("US/Mountain", "US/Mountain"),
|
||||
("US/Pacific", "US/Pacific"),
|
||||
("US/Alaska", "US/Alaska"),
|
||||
("US/Hawaii", "US/Hawaii"),
|
||||
("US/Aleutian", "US/Aleutian"),
|
||||
("US/Samoa", "US/Samoa"),
|
||||
]
|
||||
|
||||
new_preferred_timezone = GovukRadiosField(
|
||||
"What timezone would you like to use?",
|
||||
default="US/Eastern",
|
||||
)
|
||||
|
||||
|
||||
class ChangeEmailForm(StripWhitespaceForm):
|
||||
def __init__(self, validate_email_func, *args, **kwargs):
|
||||
self.validate_email_func = validate_email_func
|
||||
@@ -1323,6 +1337,49 @@ class CreateKeyForm(StripWhitespaceForm):
|
||||
raise ValidationError("A key with this name already exists")
|
||||
|
||||
|
||||
class SupportType(StripWhitespaceForm):
|
||||
support_type = GovukRadiosField(
|
||||
"How can we help you?",
|
||||
choices=[
|
||||
(PROBLEM_TICKET_TYPE, "Report a problem"),
|
||||
(QUESTION_TICKET_TYPE, "Ask a question or give feedback"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class SupportRedirect(StripWhitespaceForm):
|
||||
who = GovukRadiosField(
|
||||
"What do you need help with?",
|
||||
choices=[
|
||||
(
|
||||
"public-sector",
|
||||
"I work in the public sector and need to send emails or text messages",
|
||||
),
|
||||
("public", "I’m a member of the public with a question for the government"),
|
||||
],
|
||||
param_extensions={"fieldset": {"legend": {"classes": "usa-sr-only"}}},
|
||||
)
|
||||
|
||||
|
||||
class FeedbackOrProblem(StripWhitespaceForm):
|
||||
name = GovukTextInputField("Name (optional)")
|
||||
email_address = email_address(label="Email address", gov_user=False, required=True)
|
||||
feedback = TextAreaField(
|
||||
"Your message", validators=[DataRequired(message="Cannot be empty")]
|
||||
)
|
||||
|
||||
|
||||
class Triage(StripWhitespaceForm):
|
||||
severe = GovukRadiosField(
|
||||
"Is it an emergency?",
|
||||
choices=[
|
||||
("yes", "Yes"),
|
||||
("no", "No"),
|
||||
],
|
||||
thing="yes or no",
|
||||
)
|
||||
|
||||
|
||||
class EstimateUsageForm(StripWhitespaceForm):
|
||||
volume_email = ForgivingIntegerField(
|
||||
"How many emails do you expect to send in the next year?",
|
||||
@@ -1358,6 +1415,52 @@ class EstimateUsageForm(StripWhitespaceForm):
|
||||
return super().validate(*args, **kwargs)
|
||||
|
||||
|
||||
class AdminProviderRatioForm(Form):
|
||||
def __init__(self, providers):
|
||||
self._providers = providers
|
||||
|
||||
# hack: https://github.com/wtforms/wtforms/issues/736
|
||||
self._unbound_fields = [
|
||||
(
|
||||
provider["identifier"],
|
||||
GovukIntegerField(
|
||||
f"{provider['display_name']} (%)",
|
||||
validators=[
|
||||
validators.NumberRange(
|
||||
min=0, max=100, message="Must be between 0 and 100"
|
||||
)
|
||||
],
|
||||
param_extensions={
|
||||
"classes": "width-8",
|
||||
},
|
||||
),
|
||||
)
|
||||
for provider in providers
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
data={
|
||||
provider["identifier"]: provider["priority"] for provider in providers
|
||||
}
|
||||
)
|
||||
|
||||
def validate(self, extra_validators=None):
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
total = sum(
|
||||
getattr(self, provider["identifier"]).data for provider in self._providers
|
||||
)
|
||||
|
||||
if total == 100:
|
||||
return True
|
||||
|
||||
for provider in self._providers:
|
||||
getattr(self, provider["identifier"]).errors += ["Must add up to 100%"]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ServiceContactDetailsForm(StripWhitespaceForm):
|
||||
contact_details_type = RadioField(
|
||||
"Type of contact details",
|
||||
@@ -1461,6 +1564,65 @@ class ServiceSwitchChannelForm(ServiceOnOffSettingForm):
|
||||
super().__init__(name, *args, **kwargs)
|
||||
|
||||
|
||||
class AdminSetEmailBrandingForm(StripWhitespaceForm):
|
||||
branding_style = GovukRadiosFieldWithNoneOption(
|
||||
"Branding style",
|
||||
param_extensions={"fieldset": {"legend": {"classes": "usa-sr-only"}}},
|
||||
thing="a branding style",
|
||||
)
|
||||
|
||||
DEFAULT = (FieldWithNoneOption.NONE_OPTION_VALUE, "GOV.UK")
|
||||
|
||||
def __init__(self, all_branding_options, current_branding):
|
||||
super().__init__(branding_style=current_branding)
|
||||
|
||||
self.branding_style.choices = sorted(
|
||||
all_branding_options + [self.DEFAULT],
|
||||
key=lambda branding: (
|
||||
branding[0] != current_branding,
|
||||
branding[0] is not self.DEFAULT[0],
|
||||
branding[1].lower(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AdminPreviewBrandingForm(StripWhitespaceForm):
|
||||
branding_style = HiddenFieldWithNoneOption("branding_style")
|
||||
|
||||
|
||||
class AdminEditEmailBrandingForm(StripWhitespaceForm):
|
||||
name = GovukTextInputField("Name of brand")
|
||||
text = GovukTextInputField("Text")
|
||||
colour = GovukTextInputField(
|
||||
"Colour",
|
||||
validators=[
|
||||
Regexp(
|
||||
regex="^$|^#(?:[0-9a-fA-F]{3}){1,2}$",
|
||||
message="Must be a valid color hex code (starting with #)",
|
||||
)
|
||||
],
|
||||
param_extensions={
|
||||
"attributes": {"data-module": "colour-preview"},
|
||||
},
|
||||
)
|
||||
file = FileField_wtf(
|
||||
"Upload a PNG logo", validators=[FileAllowed(["png"], "PNG Images only!")]
|
||||
)
|
||||
brand_type = GovukRadiosField(
|
||||
"Brand type",
|
||||
choices=[
|
||||
("both", "GOV.UK and branding"),
|
||||
("org", "Branding only"),
|
||||
("org_banner", "Branding banner"),
|
||||
],
|
||||
)
|
||||
|
||||
def validate_name(self, name):
|
||||
op = request.form.get("operation")
|
||||
if op == "email-branding-details" and not self.name.data:
|
||||
raise ValidationError("This field is required")
|
||||
|
||||
|
||||
class SVGFileUpload(StripWhitespaceForm):
|
||||
file = FileField_wtf(
|
||||
"Upload an SVG logo",
|
||||
@@ -1675,6 +1837,42 @@ class AdminSetOrganizationForm(StripWhitespaceForm):
|
||||
)
|
||||
|
||||
|
||||
class ChooseBrandingForm(StripWhitespaceForm):
|
||||
FALLBACK_OPTION_VALUE = "something_else"
|
||||
FALLBACK_OPTION = (FALLBACK_OPTION_VALUE, "Something else")
|
||||
|
||||
@property
|
||||
def something_else_is_only_option(self):
|
||||
return self.options.choices == (self.FALLBACK_OPTION,)
|
||||
|
||||
|
||||
class ChooseEmailBrandingForm(ChooseBrandingForm):
|
||||
options = RadioField("Choose your new email branding")
|
||||
|
||||
def __init__(self, service):
|
||||
super().__init__()
|
||||
|
||||
self.options.choices = tuple(
|
||||
list(branding.get_email_choices(service)) + [self.FALLBACK_OPTION]
|
||||
)
|
||||
|
||||
|
||||
class SomethingElseBrandingForm(StripWhitespaceForm):
|
||||
something_else = GovukTextareaField(
|
||||
"Describe the branding you want",
|
||||
validators=[DataRequired("Cannot be empty")],
|
||||
param_extensions={
|
||||
"label": {
|
||||
"isPageHeading": True,
|
||||
"classes": "font-body-xl",
|
||||
},
|
||||
"hint": {
|
||||
"text": "Include links to your brand guidelines or examples of how to use your branding."
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AdminServiceAddDataRetentionForm(StripWhitespaceForm):
|
||||
notification_type = GovukRadiosField(
|
||||
"What notification type?",
|
||||
@@ -1866,6 +2064,13 @@ class AdminClearCacheForm(StripWhitespaceForm):
|
||||
raise ValidationError("Select at least one option")
|
||||
|
||||
|
||||
class AdminOrganizationGoLiveNotesForm(StripWhitespaceForm):
|
||||
request_to_go_live_notes = TextAreaField(
|
||||
"Go live notes",
|
||||
filters=[lambda x: x or None],
|
||||
)
|
||||
|
||||
|
||||
class ChangeSecurityKeyNameForm(StripWhitespaceForm):
|
||||
security_key_name = GovukTextInputField(
|
||||
"Name of key",
|
||||
|
||||
@@ -41,8 +41,8 @@ def _create_example_template(service_id):
|
||||
|
||||
|
||||
@main.route("/add-service", methods=["GET", "POST"])
|
||||
@user_is_gov_user
|
||||
@user_is_logged_in
|
||||
@user_is_gov_user
|
||||
def add_service():
|
||||
default_organization_type = current_user.default_organization_type
|
||||
if default_organization_type is None:
|
||||
|
||||
@@ -11,12 +11,10 @@ from werkzeug.utils import redirect
|
||||
from app import (
|
||||
billing_api_client,
|
||||
current_service,
|
||||
job_api_client,
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
template_statistics_client,
|
||||
)
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric
|
||||
from app.main import main
|
||||
from app.statistics_utils import get_formatted_percentage
|
||||
from app.utils import (
|
||||
@@ -47,32 +45,10 @@ def service_dashboard(service_id):
|
||||
if not current_user.has_permissions("view_activity"):
|
||||
return redirect(url_for("main.choose_template", service_id=service_id))
|
||||
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
service_id=service_id,
|
||||
)["notifications"]
|
||||
|
||||
job_response = job_api_client.get_jobs(service_id)
|
||||
|
||||
service_data_retention_days = 7
|
||||
jobs = [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
"download_link": url_for(
|
||||
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"notification_count": job["notification_count"],
|
||||
"created_by": job["created_by"],
|
||||
}
|
||||
for job in job_response["data"]
|
||||
]
|
||||
return render_template(
|
||||
"views/dashboard/dashboard.html",
|
||||
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||
partials=get_dashboard_partials(service_id),
|
||||
notifications=notifications,
|
||||
jobs=jobs,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
)
|
||||
|
||||
|
||||
|
||||
133
app/main/views/email_branding.py
Normal file
133
app/main/views/email_branding.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from flask import current_app, redirect, render_template, session, url_for
|
||||
|
||||
from app import email_branding_client
|
||||
from app.main import main
|
||||
from app.main.forms import AdminEditEmailBrandingForm, SearchByNameForm
|
||||
from app.s3_client.s3_logo_client import (
|
||||
TEMP_TAG,
|
||||
delete_email_temp_file,
|
||||
delete_email_temp_files_created_by,
|
||||
permanent_email_logo_name,
|
||||
persist_logo,
|
||||
upload_email_logo,
|
||||
)
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
|
||||
@main.route("/email-branding", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def email_branding():
|
||||
brandings = email_branding_client.get_all_email_branding(sort_key="name")
|
||||
|
||||
return render_template(
|
||||
"views/email-branding/select-branding.html",
|
||||
email_brandings=brandings,
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit", methods=["GET", "POST"])
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit/<logo>", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def update_email_branding(branding_id, logo=None):
|
||||
email_branding = email_branding_client.get_email_branding(branding_id)[
|
||||
"email_branding"
|
||||
]
|
||||
|
||||
form = AdminEditEmailBrandingForm(
|
||||
name=email_branding["name"],
|
||||
text=email_branding["text"],
|
||||
colour=email_branding["colour"],
|
||||
brand_type=email_branding["brand_type"],
|
||||
)
|
||||
|
||||
logo = logo if logo else email_branding.get("logo") if email_branding else None
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.file.data:
|
||||
upload_filename = upload_email_logo(
|
||||
form.file.data.filename, form.file.data, user_id=session["user_id"]
|
||||
)
|
||||
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
|
||||
delete_email_temp_file(logo)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".update_email_branding",
|
||||
branding_id=branding_id,
|
||||
logo=upload_filename,
|
||||
)
|
||||
)
|
||||
|
||||
updated_logo_name = (
|
||||
permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
)
|
||||
|
||||
email_branding_client.update_email_branding(
|
||||
branding_id=branding_id,
|
||||
logo=updated_logo_name,
|
||||
name=form.name.data,
|
||||
text=form.text.data,
|
||||
colour=form.colour.data,
|
||||
brand_type=form.brand_type.data,
|
||||
)
|
||||
|
||||
if logo:
|
||||
persist_logo(logo, updated_logo_name)
|
||||
|
||||
delete_email_temp_files_created_by(session["user_id"])
|
||||
|
||||
return redirect(url_for(".email_branding", branding_id=branding_id))
|
||||
|
||||
return render_template(
|
||||
"views/email-branding/manage-branding.html",
|
||||
form=form,
|
||||
email_branding=email_branding,
|
||||
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
|
||||
logo=logo,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/email-branding/create", methods=["GET", "POST"])
|
||||
@main.route("/email-branding/create/<logo>", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def create_email_branding(logo=None):
|
||||
form = AdminEditEmailBrandingForm(brand_type="org")
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.file.data:
|
||||
upload_filename = upload_email_logo(
|
||||
form.file.data.filename, form.file.data, user_id=session["user_id"]
|
||||
)
|
||||
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
|
||||
delete_email_temp_file(logo)
|
||||
|
||||
return redirect(url_for(".create_email_branding", logo=upload_filename))
|
||||
|
||||
updated_logo_name = (
|
||||
permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
)
|
||||
|
||||
email_branding_client.create_email_branding(
|
||||
logo=updated_logo_name,
|
||||
name=form.name.data,
|
||||
text=form.text.data,
|
||||
colour=form.colour.data,
|
||||
brand_type=form.brand_type.data,
|
||||
)
|
||||
|
||||
if logo:
|
||||
persist_logo(logo, updated_logo_name)
|
||||
|
||||
delete_email_temp_files_created_by(session["user_id"])
|
||||
|
||||
return redirect(url_for(".email_branding"))
|
||||
|
||||
return render_template(
|
||||
"views/email-branding/manage-branding.html",
|
||||
form=form,
|
||||
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
|
||||
logo=logo,
|
||||
)
|
||||
@@ -1,10 +1,239 @@
|
||||
from flask import render_template
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from flask import redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from govuk_bank_holidays.bank_holidays import BankHolidays
|
||||
from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket
|
||||
|
||||
from app import convert_to_boolean, current_service
|
||||
from app.extensions import zendesk_client
|
||||
from app.main import main
|
||||
from app.utils.user import user_is_logged_in
|
||||
from app.main.forms import FeedbackOrProblem, SupportRedirect, SupportType, Triage
|
||||
from app.models.feedback import (
|
||||
GENERAL_TICKET_TYPE,
|
||||
PROBLEM_TICKET_TYPE,
|
||||
QUESTION_TICKET_TYPE,
|
||||
)
|
||||
from app.utils import hide_from_search_engines
|
||||
|
||||
bank_holidays = BankHolidays(use_cached_holidays=True)
|
||||
|
||||
|
||||
@main.route("/support", methods=["GET"])
|
||||
@user_is_logged_in
|
||||
@main.route("/support", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def support():
|
||||
return render_template("views/support/index.html")
|
||||
if current_user.is_authenticated:
|
||||
form = SupportType()
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(
|
||||
".feedback",
|
||||
ticket_type=form.support_type.data,
|
||||
)
|
||||
)
|
||||
else:
|
||||
form = SupportRedirect()
|
||||
if form.validate_on_submit():
|
||||
if form.who.data == "public":
|
||||
return redirect(url_for(".support_public"))
|
||||
else:
|
||||
return redirect(
|
||||
url_for(
|
||||
".feedback",
|
||||
ticket_type=GENERAL_TICKET_TYPE,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template("views/support/index.html", form=form)
|
||||
|
||||
|
||||
@main.route("/support/public")
|
||||
@hide_from_search_engines
|
||||
def support_public():
|
||||
return render_template("views/support/public.html")
|
||||
|
||||
|
||||
@main.route("/support/triage", methods=["GET", "POST"])
|
||||
@main.route("/support/triage/<ticket_type:ticket_type>", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def triage(ticket_type=PROBLEM_TICKET_TYPE):
|
||||
form = Triage()
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(".feedback", ticket_type=ticket_type, severe=form.severe.data)
|
||||
)
|
||||
return render_template(
|
||||
"views/support/triage.html",
|
||||
form=form,
|
||||
page_title={
|
||||
PROBLEM_TICKET_TYPE: "Report a problem",
|
||||
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
|
||||
}.get(ticket_type),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/support/<ticket_type:ticket_type>", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def feedback(ticket_type):
|
||||
form = FeedbackOrProblem()
|
||||
|
||||
if not form.feedback.data:
|
||||
form.feedback.data = session.pop("feedback_message", "")
|
||||
|
||||
if request.args.get("severe") in ["yes", "no"]:
|
||||
severe = convert_to_boolean(request.args.get("severe"))
|
||||
else:
|
||||
severe = None
|
||||
|
||||
out_of_hours_emergency = all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
not in_business_hours(),
|
||||
severe,
|
||||
)
|
||||
)
|
||||
|
||||
if needs_triage(ticket_type, severe):
|
||||
session["feedback_message"] = form.feedback.data
|
||||
return redirect(url_for(".triage", ticket_type=ticket_type))
|
||||
|
||||
if needs_escalation(ticket_type, severe):
|
||||
return redirect(url_for(".bat_phone"))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
form.email_address.data = current_user.email_address
|
||||
form.name.data = current_user.name
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_email = form.email_address.data
|
||||
user_name = form.name.data or None
|
||||
|
||||
feedback_msg = render_template(
|
||||
"support-tickets/support-ticket.txt",
|
||||
content=form.feedback.data,
|
||||
)
|
||||
|
||||
ticket = NotifySupportTicket(
|
||||
subject="Notify feedback",
|
||||
message=feedback_msg,
|
||||
ticket_type=get_zendesk_ticket_type(ticket_type),
|
||||
p1=out_of_hours_emergency,
|
||||
user_name=user_name,
|
||||
user_email=user_email,
|
||||
org_id=current_service.organization_id if current_service else None,
|
||||
org_type=current_service.organization_type if current_service else None,
|
||||
service_id=current_service.id if current_service else None,
|
||||
)
|
||||
zendesk_client.send_ticket_to_zendesk(ticket)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".thanks",
|
||||
out_of_hours_emergency=out_of_hours_emergency,
|
||||
email_address_provided=(
|
||||
current_user.is_authenticated or bool(form.email_address.data)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/support/form.html",
|
||||
form=form,
|
||||
back_link=(
|
||||
url_for(".support")
|
||||
if severe is None
|
||||
else url_for(".triage", ticket_type=ticket_type)
|
||||
),
|
||||
show_status_page_banner=(ticket_type == PROBLEM_TICKET_TYPE),
|
||||
page_title={
|
||||
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
|
||||
PROBLEM_TICKET_TYPE: "Report a problem",
|
||||
QUESTION_TICKET_TYPE: "Ask a question or give feedback",
|
||||
}.get(ticket_type),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/support/escalate", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def bat_phone():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("main.feedback", ticket_type=PROBLEM_TICKET_TYPE))
|
||||
|
||||
return render_template("views/support/bat-phone.html")
|
||||
|
||||
|
||||
@main.route("/support/thanks", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def thanks():
|
||||
return render_template(
|
||||
"views/support/thanks.html",
|
||||
out_of_hours_emergency=convert_to_boolean(
|
||||
request.args.get("out_of_hours_emergency")
|
||||
),
|
||||
email_address_provided=convert_to_boolean(
|
||||
request.args.get("email_address_provided")
|
||||
),
|
||||
out_of_hours=not in_business_hours(),
|
||||
)
|
||||
|
||||
|
||||
def in_business_hours():
|
||||
now = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
|
||||
if is_weekend(now) or is_bank_holiday(now):
|
||||
return False
|
||||
|
||||
return london_time_today_as_utc(9, 30) <= now < london_time_today_as_utc(17, 30)
|
||||
|
||||
|
||||
def london_time_today_as_utc(hour, minute):
|
||||
return (
|
||||
pytz.timezone("Europe/London")
|
||||
.localize(datetime.now().replace(hour=hour, minute=minute))
|
||||
.astimezone(pytz.utc)
|
||||
)
|
||||
|
||||
|
||||
def is_weekend(time):
|
||||
return time.strftime("%A") in {
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
}
|
||||
|
||||
|
||||
def is_bank_holiday(time):
|
||||
return bank_holidays.is_holiday(time.date())
|
||||
|
||||
|
||||
def needs_triage(ticket_type, severe):
|
||||
return all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe is None,
|
||||
(not current_user.is_authenticated or current_user.live_services),
|
||||
not in_business_hours(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def needs_escalation(ticket_type, severe):
|
||||
return all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe,
|
||||
not current_user.is_authenticated,
|
||||
not in_business_hours(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_zendesk_ticket_type(ticket_type):
|
||||
# Zendesk has 4 ticket types - "problem", "incident", "task" and "question".
|
||||
# We don't want to use a Zendesk "problem" ticket type when someone reports a
|
||||
# Notify problem because they are designed to group multiple incident tickets together,
|
||||
# allowing them to be solved as a group.
|
||||
if ticket_type == PROBLEM_TICKET_TYPE:
|
||||
return NotifySupportTicket.TYPE_INCIDENT
|
||||
|
||||
return NotifySupportTicket.TYPE_QUESTION
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
from flask import abort, redirect, render_template, request, url_for
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
make_response,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from notifications_utils.template import HTMLEmailTemplate
|
||||
|
||||
from app import status_api_client
|
||||
from app.formatters import apply_html_class, convert_markdown_template
|
||||
from app import email_branding_client, status_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import FieldWithNoneOption
|
||||
from app.main.views.pricing import CURRENT_SMS_RATE
|
||||
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
|
||||
from app.utils.user import user_is_logged_in
|
||||
@@ -54,6 +63,105 @@ def design_content():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/_email")
|
||||
@user_is_logged_in
|
||||
def email_template():
|
||||
branding_type = "govuk"
|
||||
branding_style = request.args.get("branding_style", None)
|
||||
|
||||
if branding_style == FieldWithNoneOption.NONE_OPTION_VALUE:
|
||||
branding_style = None
|
||||
|
||||
if branding_style is not None:
|
||||
email_branding = email_branding_client.get_email_branding(branding_style)[
|
||||
"email_branding"
|
||||
]
|
||||
branding_type = email_branding["brand_type"]
|
||||
|
||||
if branding_type == "govuk":
|
||||
brand_text = None
|
||||
brand_colour = None
|
||||
brand_logo = None
|
||||
govuk_banner = True
|
||||
brand_banner = False
|
||||
brand_name = None
|
||||
else:
|
||||
colour = email_branding["colour"]
|
||||
brand_text = email_branding["text"]
|
||||
brand_colour = colour
|
||||
brand_logo = (
|
||||
f"https://{current_app.config['LOGO_CDN_DOMAIN']}/{email_branding['logo']}"
|
||||
if email_branding["logo"]
|
||||
else None
|
||||
)
|
||||
govuk_banner = branding_type in ["govuk", "both"]
|
||||
brand_banner = branding_type == "org_banner"
|
||||
brand_name = email_branding["name"]
|
||||
|
||||
template = {
|
||||
"template_type": "email",
|
||||
"subject": "Email branding preview",
|
||||
"content": (
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting "
|
||||
"industry.\n\nLorem Ipsum has been the industry’s standard dummy "
|
||||
"text ever since the 1500s, when an unknown printer took a galley "
|
||||
"of type and scrambled it to make a type specimen book. "
|
||||
"\n\n"
|
||||
"# History"
|
||||
"\n\n"
|
||||
"It has "
|
||||
"survived not only"
|
||||
"\n\n"
|
||||
"* five centuries"
|
||||
"\n"
|
||||
"* but also the leap into electronic typesetting"
|
||||
"\n\n"
|
||||
"It was "
|
||||
"popularised in the 1960s with the release of Letraset sheets "
|
||||
"containing Lorem Ipsum passages, and more recently with desktop "
|
||||
"publishing software like Aldus PageMaker including versions of "
|
||||
"Lorem Ipsum."
|
||||
"\n\n"
|
||||
"^ It is a long established fact that a reader will be distracted "
|
||||
"by the readable content of a page when looking at its layout."
|
||||
"\n\n"
|
||||
"The point of using Lorem Ipsum is that it has a more-or-less "
|
||||
"normal distribution of letters, as opposed to using ‘Content "
|
||||
"here, content here’, making it look like readable English."
|
||||
"\n\n\n"
|
||||
"1. One"
|
||||
"\n"
|
||||
"2. Two"
|
||||
"\n"
|
||||
"10. Three"
|
||||
"\n\n"
|
||||
"This is an example of an email sent using Notify.gov."
|
||||
"\n\n"
|
||||
"https://www.notifications.service.gov.uk"
|
||||
),
|
||||
}
|
||||
|
||||
if not bool(request.args):
|
||||
resp = make_response(str(HTMLEmailTemplate(template)))
|
||||
else:
|
||||
resp = make_response(
|
||||
str(
|
||||
HTMLEmailTemplate(
|
||||
template,
|
||||
govuk_banner=govuk_banner,
|
||||
brand_text=brand_text,
|
||||
brand_colour=brand_colour,
|
||||
brand_logo=brand_logo,
|
||||
brand_banner=brand_banner,
|
||||
brand_name=brand_name,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
resp.headers["X-Frame-Options"] = "SAMEORIGIN"
|
||||
return resp
|
||||
|
||||
|
||||
@main.route("/documentation")
|
||||
@user_is_logged_in
|
||||
def documentation():
|
||||
@@ -88,6 +196,14 @@ def roadmap():
|
||||
return render_template("views/roadmap.html", navigation_links=features_nav())
|
||||
|
||||
|
||||
@main.route("/features/email")
|
||||
@user_is_logged_in
|
||||
def features_email():
|
||||
return render_template(
|
||||
"views/features/emails.html", navigation_links=features_nav()
|
||||
)
|
||||
|
||||
|
||||
@main.route("/features/sms")
|
||||
@user_is_logged_in
|
||||
def features_sms():
|
||||
@@ -138,20 +254,9 @@ def get_started_old():
|
||||
@main.route("/using-notify/get-started")
|
||||
@user_is_logged_in
|
||||
def get_started():
|
||||
markdown = convert_markdown_template("get-started")
|
||||
html_style = apply_html_class(
|
||||
[
|
||||
["ol", "usa-process-list"],
|
||||
["li", "usa-process-list__item padding-bottom-4 margin-top-2"],
|
||||
["h2", "usa-process-list__heading line-height-sans-3"],
|
||||
],
|
||||
markdown,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/get-started.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
content=html_style,
|
||||
)
|
||||
|
||||
|
||||
@@ -183,6 +288,15 @@ def guidance_index():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/using-notify/guidance/branding-and-customisation")
|
||||
@user_is_logged_in
|
||||
def branding_and_customisation():
|
||||
return render_template(
|
||||
"views/guidance/branding-and-customisation.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/using-notify/guidance/create-and-send-messages")
|
||||
@user_is_logged_in
|
||||
def create_and_send_messages():
|
||||
|
||||
@@ -9,7 +9,6 @@ from app.event_handlers import (
|
||||
create_invite_user_to_service_event,
|
||||
create_mobile_number_change_event,
|
||||
create_remove_user_from_service_event,
|
||||
create_resend_user_invite_to_service_event,
|
||||
)
|
||||
from app.formatters import redact_mobile_number
|
||||
from app.main import main
|
||||
@@ -46,6 +45,7 @@ def manage_users(service_id):
|
||||
@user_has_permissions("manage_service")
|
||||
def invite_user(service_id, user_id=None):
|
||||
form_class = InviteUserForm
|
||||
|
||||
form = form_class(
|
||||
inviter_email_address=current_user.email_address,
|
||||
all_template_folders=current_service.all_template_folders,
|
||||
@@ -332,22 +332,3 @@ def cancel_invited_user(service_id, invited_user_id):
|
||||
|
||||
flash(f"Invitation cancelled for {invited_user.email_address}", "default_with_tick")
|
||||
return redirect(url_for("main.manage_users", service_id=service_id))
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/resend-invite/<uuid:invited_user_id>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def resend_invite(service_id, invited_user_id):
|
||||
current_service.resend_invite(invited_user_id)
|
||||
|
||||
invited_user = InvitedUser.by_id_and_service_id(service_id, invited_user_id)
|
||||
create_resend_user_invite_to_service_event(
|
||||
email_address=invited_user.email_address,
|
||||
resent_by_id=current_user.id,
|
||||
service_id=service_id,
|
||||
)
|
||||
|
||||
flash(f"Invitation resent for {invited_user.email_address}", "default_with_tick")
|
||||
return redirect(url_for("main.manage_users", service_id=service_id))
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
flash,
|
||||
jsonify,
|
||||
render_template,
|
||||
request,
|
||||
@@ -11,7 +10,12 @@ from flask import (
|
||||
url_for,
|
||||
)
|
||||
|
||||
from app import current_service, job_api_client, notification_api_client
|
||||
from app import (
|
||||
current_service,
|
||||
format_date_numeric,
|
||||
job_api_client,
|
||||
notification_api_client,
|
||||
)
|
||||
from app.main import main
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_TEST
|
||||
from app.utils import (
|
||||
@@ -21,23 +25,21 @@ from app.utils import (
|
||||
parse_filter_args,
|
||||
set_status_filters,
|
||||
)
|
||||
from app.utils.csv import generate_notifications_csv, get_user_preferred_timezone
|
||||
from app.utils.csv import generate_notifications_csv
|
||||
from app.utils.templates import get_template
|
||||
from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
def view_notification(service_id, notification_id, error_message=None):
|
||||
if error_message:
|
||||
flash(error_message)
|
||||
|
||||
def view_notification(service_id, notification_id):
|
||||
notification = notification_api_client.get_notification(
|
||||
service_id, str(notification_id)
|
||||
)
|
||||
notification["template"].update({"reply_to_text": notification["reply_to_text"]})
|
||||
|
||||
personalisation = get_all_personalisation_from_notification(notification)
|
||||
error_message = None
|
||||
|
||||
template = get_template(
|
||||
notification["template"],
|
||||
@@ -141,9 +143,6 @@ def download_notifications_csv(service_id):
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
filter_args.get("message_type")[0]
|
||||
)
|
||||
file_time = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
|
||||
file_time = f"{file_time} {get_user_preferred_timezone()}"
|
||||
|
||||
return Response(
|
||||
stream_with_context(
|
||||
generate_notifications_csv(
|
||||
@@ -160,7 +159,7 @@ def download_notifications_csv(service_id):
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
|
||||
file_time,
|
||||
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
|
||||
filter_args["message_type"][0],
|
||||
current_service.name,
|
||||
)
|
||||
|
||||
@@ -6,13 +6,21 @@ from flask import flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import current_organization, org_invite_api_client, organizations_client
|
||||
from app import (
|
||||
current_organization,
|
||||
email_branding_client,
|
||||
org_invite_api_client,
|
||||
organizations_client,
|
||||
)
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
AdminBillingDetailsForm,
|
||||
AdminNewOrganizationForm,
|
||||
AdminNotesForm,
|
||||
AdminOrganizationDomainsForm,
|
||||
AdminOrganizationGoLiveNotesForm,
|
||||
AdminPreviewBrandingForm,
|
||||
AdminSetEmailBrandingForm,
|
||||
InviteOrgUserForm,
|
||||
OrganizationOrganizationTypeForm,
|
||||
RenameOrganizationForm,
|
||||
@@ -23,6 +31,7 @@ from app.main.views.dashboard import (
|
||||
get_tuples_of_financial_years,
|
||||
requested_and_current_financial_year,
|
||||
)
|
||||
from app.main.views.service_settings import get_branding_as_value_and_label
|
||||
from app.models.organization import AllOrganizations, Organization
|
||||
from app.models.user import InvitedOrgUser, User
|
||||
from app.utils.csv import Spreadsheet
|
||||
@@ -274,6 +283,58 @@ def edit_organization_type(org_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/set-email-branding", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_email_branding(org_id):
|
||||
email_branding = email_branding_client.get_all_email_branding()
|
||||
|
||||
form = AdminSetEmailBrandingForm(
|
||||
all_branding_options=get_branding_as_value_and_label(email_branding),
|
||||
current_branding=current_organization.email_branding_id,
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(
|
||||
".organization_preview_email_branding",
|
||||
org_id=org_id,
|
||||
branding_style=form.branding_style.data,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/organizations/organization/settings/set-email-branding.html",
|
||||
form=form,
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/preview-email-branding",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def organization_preview_email_branding(org_id):
|
||||
branding_style = request.args.get("branding_style", None)
|
||||
|
||||
form = AdminPreviewBrandingForm(branding_style=branding_style)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_organization.update(
|
||||
email_branding_id=form.branding_style.data,
|
||||
delete_services_cache=True,
|
||||
)
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
"views/organizations/organization/settings/preview-email-branding.html",
|
||||
form=form,
|
||||
action=url_for("main.organization_preview_email_branding", org_id=org_id),
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/edit-organization-domains",
|
||||
methods=["GET", "POST"],
|
||||
@@ -312,6 +373,28 @@ def edit_organization_domains(org_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/edit-go-live-notes", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_go_live_notes(org_id):
|
||||
form = AdminOrganizationGoLiveNotesForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
organizations_client.update_organization(
|
||||
org_id, request_to_go_live_notes=form.request_to_go_live_notes.data
|
||||
)
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
org = organizations_client.get_organization(org_id)
|
||||
form.request_to_go_live_notes.data = org["request_to_go_live_notes"]
|
||||
|
||||
return render_template(
|
||||
"views/organizations/organization/settings/edit-go-live-notes.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/notes", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_organization_notes(org_id):
|
||||
|
||||
@@ -8,15 +8,13 @@ from flask import render_template
|
||||
|
||||
from app import performance_dashboard_api_client, status_api_client
|
||||
from app.main import main
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
|
||||
|
||||
@main.route("/performance")
|
||||
def performance():
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
stats = performance_dashboard_api_client.get_performance_dashboard_stats(
|
||||
start_date=(datetime.now(preferred_tz) - timedelta(days=7)).date(),
|
||||
end_date=datetime.now(preferred_tz).date(),
|
||||
start_date=(datetime.now(pytz.utc) - timedelta(days=7)).date(),
|
||||
end_date=datetime.now(pytz.utc).date(),
|
||||
)
|
||||
stats["organizations_using_notify"] = sorted(
|
||||
[
|
||||
@@ -37,5 +35,4 @@ def performance():
|
||||
stats[
|
||||
"count_of_live_services_and_organizations"
|
||||
] = status_api_client.get_count_of_live_services_and_organizations()
|
||||
|
||||
return render_template("views/performance.html", **stats)
|
||||
|
||||
@@ -596,6 +596,13 @@ def clear_cache():
|
||||
"service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-versions", # noqa
|
||||
],
|
||||
),
|
||||
(
|
||||
"email_branding",
|
||||
[
|
||||
"email_branding",
|
||||
"email_branding-????????-????-????-????-????????????",
|
||||
],
|
||||
),
|
||||
(
|
||||
"organization",
|
||||
[
|
||||
@@ -682,6 +689,7 @@ def format_stats_by_service(services):
|
||||
"name": service["name"],
|
||||
"stats": service["statistics"],
|
||||
"restricted": service["restricted"],
|
||||
"research_mode": service["research_mode"],
|
||||
"created_at": service["created_at"],
|
||||
"active": service["active"],
|
||||
}
|
||||
|
||||
79
app/main/views/providers.py
Normal file
79
app/main/views/providers.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from datetime import datetime
|
||||
|
||||
from flask import render_template, url_for
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from app import provider_client
|
||||
from app.main import main
|
||||
from app.main.forms import AdminProviderRatioForm
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
PROVIDER_PRIORITY_MEANING_SWITCHOVER = datetime(2019, 11, 29, 11, 0).isoformat()
|
||||
|
||||
|
||||
@main.route("/providers")
|
||||
@user_is_platform_admin
|
||||
def view_providers():
|
||||
providers = provider_client.get_all_providers()["provider_details"]
|
||||
domestic_email_providers, domestic_sms_providers, intl_sms_providers = [], [], []
|
||||
for provider in providers:
|
||||
if provider["notification_type"] == "sms":
|
||||
domestic_sms_providers.append(provider)
|
||||
if provider.get("supports_international", None):
|
||||
intl_sms_providers.append(provider)
|
||||
elif provider["notification_type"] == "email":
|
||||
domestic_email_providers.append(provider)
|
||||
|
||||
add_monthly_traffic(domestic_sms_providers)
|
||||
|
||||
return render_template(
|
||||
"views/providers/providers.html",
|
||||
email_providers=domestic_email_providers,
|
||||
domestic_sms_providers=domestic_sms_providers,
|
||||
intl_sms_providers=intl_sms_providers,
|
||||
)
|
||||
|
||||
|
||||
def add_monthly_traffic(domestic_sms_providers):
|
||||
total_sms_sent = sum(
|
||||
provider["current_month_billable_sms"] for provider in domestic_sms_providers
|
||||
)
|
||||
|
||||
for provider in domestic_sms_providers:
|
||||
percentage = (
|
||||
(provider["current_month_billable_sms"] / total_sms_sent * 100)
|
||||
if total_sms_sent
|
||||
else 0
|
||||
)
|
||||
provider["monthly_traffic"] = round(percentage)
|
||||
|
||||
|
||||
@main.route("/provider/edit-sms-provider-ratio", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_sms_provider_ratio():
|
||||
providers = [
|
||||
provider
|
||||
for provider in provider_client.get_all_providers()["provider_details"]
|
||||
if provider["notification_type"] == "sms" and provider["active"]
|
||||
]
|
||||
|
||||
form = AdminProviderRatioForm(providers)
|
||||
|
||||
if form.validate_on_submit():
|
||||
for provider in providers:
|
||||
field = getattr(form, provider["identifier"])
|
||||
provider_client.update_provider(provider["id"], field.data)
|
||||
return redirect(url_for(".view_providers"))
|
||||
|
||||
return render_template(
|
||||
"views/providers/edit-sms-provider-ratio.html", form=form, providers=providers
|
||||
)
|
||||
|
||||
|
||||
@main.route("/provider/<uuid:provider_id>")
|
||||
@user_is_platform_admin
|
||||
def view_provider(provider_id):
|
||||
versions = provider_client.get_provider_versions(provider_id)
|
||||
return render_template(
|
||||
"views/providers/provider.html", provider_versions=versions["data"]
|
||||
)
|
||||
@@ -1,10 +1,17 @@
|
||||
import itertools
|
||||
import time
|
||||
import uuid
|
||||
from string import ascii_uppercase
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from flask import abort, flash, redirect, render_template, request, session, url_for
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
||||
@@ -203,8 +210,6 @@ def set_sender(service_id, template_id):
|
||||
|
||||
sender_details = get_sender_details(service_id, template["template_type"])
|
||||
|
||||
sender_details = remove_notify_from_sender_options(sender_details)
|
||||
|
||||
if len(sender_details) == 1:
|
||||
session["sender_id"] = sender_details[0]["id"]
|
||||
|
||||
@@ -257,25 +262,6 @@ def set_sender(service_id, template_id):
|
||||
)
|
||||
|
||||
|
||||
def remove_notify_from_sender_options(sender_details):
|
||||
# Remove US Notify/Notify.gov from users list of sender
|
||||
# options during message send flow
|
||||
sender_details = [
|
||||
sender for sender in sender_details if verify_sender_options(sender)
|
||||
]
|
||||
|
||||
return sender_details
|
||||
|
||||
|
||||
def verify_sender_options(sender):
|
||||
if sender.get("sms_sender") in ["Notify.gov", "US Notify"] and sender["is_default"]:
|
||||
return True
|
||||
if sender.get("sms_sender") not in ["Notify.gov", "US Notify"]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_sender_context(sender_details, template_type):
|
||||
context = {
|
||||
"email": {
|
||||
@@ -488,6 +474,7 @@ def _check_messages(service_id, template_id, upload_id, preview_row):
|
||||
remaining_messages = current_service.message_limit - notification_count
|
||||
|
||||
contents = s3download(service_id, upload_id)
|
||||
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
@@ -780,8 +767,6 @@ def _check_notification(service_id, template_id, exception=None):
|
||||
|
||||
back_link = get_back_link(service_id, template, len(placeholders), placeholders)
|
||||
|
||||
choose_time_form = ChooseTimeForm()
|
||||
|
||||
if (not session.get("recipient")) or not all_placeholders_in_session(
|
||||
template.placeholders
|
||||
):
|
||||
@@ -793,7 +778,6 @@ def _check_notification(service_id, template_id, exception=None):
|
||||
return dict(
|
||||
template=template,
|
||||
back_link=back_link,
|
||||
choose_time_form=choose_time_form,
|
||||
**(get_template_error_dict(exception) if exception else {}),
|
||||
)
|
||||
|
||||
@@ -831,6 +815,7 @@ def get_template_error_dict(exception):
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
|
||||
if not recipient:
|
||||
return redirect(
|
||||
url_for(
|
||||
@@ -840,69 +825,38 @@ def send_notification(service_id, template_id):
|
||||
)
|
||||
)
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
for k, v in session["placeholders"].items():
|
||||
keys.append(k)
|
||||
values.append(v)
|
||||
|
||||
data = ",".join(keys)
|
||||
vals = ",".join(values)
|
||||
data = f"{data}\r\n{vals}"
|
||||
|
||||
filename = f"one-off-{current_user.name}-{uuid.uuid4()}.csv"
|
||||
my_data = {"filename": filename, "template_id": template_id, "data": data}
|
||||
upload_id = s3upload(service_id, my_data)
|
||||
form = CsvUploadForm()
|
||||
form.file.data = my_data
|
||||
form.file.name = filename
|
||||
|
||||
check_message_output = check_messages(service_id, template_id, upload_id, 2)
|
||||
if "You cannot send to" in check_message_output:
|
||||
return check_messages(service_id, template_id, upload_id, 2)
|
||||
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for=request.form.get("scheduled_for", ""),
|
||||
template_id=template_id,
|
||||
original_file_name=filename,
|
||||
notification_count=1,
|
||||
valid="True",
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
|
||||
session.pop("recipient")
|
||||
session.pop("placeholders")
|
||||
|
||||
# We have to wait for the job to run and create the notification in the database
|
||||
time.sleep(0.1)
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
service_id, job_id=upload_id, include_one_off=True
|
||||
)
|
||||
attempts = 0
|
||||
while notifications["total"] == 0 and attempts < 5:
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
service_id, job_id=upload_id, include_one_off=True
|
||||
try:
|
||||
noti = notification_api_client.send_notification(
|
||||
service_id,
|
||||
template_id=db_template["id"],
|
||||
recipient=recipient,
|
||||
personalisation=session["placeholders"],
|
||||
sender_id=session.get("sender_id", None),
|
||||
)
|
||||
time.sleep(0.1)
|
||||
attempts = attempts + 1
|
||||
|
||||
if notifications["total"] == 0 and attempts == 5:
|
||||
# This shows the job we auto-generated for the user
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.view_job",
|
||||
service_id=service_id,
|
||||
job_id=upload_id,
|
||||
except HTTPError as exception:
|
||||
current_app.logger.error(
|
||||
'Service {} could not send notification: "{}"'.format(
|
||||
current_service.id, exception.message
|
||||
)
|
||||
)
|
||||
return render_template(
|
||||
"views/notifications/check.html",
|
||||
**_check_notification(service_id, template_id, exception),
|
||||
)
|
||||
|
||||
session.pop("placeholders")
|
||||
session.pop("recipient")
|
||||
session.pop("sender_id", None)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".view_notification",
|
||||
service_id=service_id,
|
||||
from_job=upload_id,
|
||||
notification_id=notifications["notifications"][0]["id"],
|
||||
notification_id=noti["id"],
|
||||
# used to show the final step of the tour (help=3) or not show
|
||||
# a back link on a just sent one off notification (help=0)
|
||||
help=request.args.get("help"),
|
||||
|
||||
@@ -13,10 +13,12 @@ from flask import (
|
||||
)
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket
|
||||
|
||||
from app import (
|
||||
billing_api_client,
|
||||
current_service,
|
||||
email_branding_client,
|
||||
inbound_number_client,
|
||||
notification_api_client,
|
||||
organizations_client,
|
||||
@@ -27,18 +29,23 @@ from app.event_handlers import (
|
||||
create_resume_service_event,
|
||||
create_suspend_service_event,
|
||||
)
|
||||
from app.extensions import zendesk_client
|
||||
from app.formatters import email_safe
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
AdminBillingDetailsForm,
|
||||
AdminNotesForm,
|
||||
AdminPreviewBrandingForm,
|
||||
AdminServiceAddDataRetentionForm,
|
||||
AdminServiceEditDataRetentionForm,
|
||||
AdminServiceInboundNumberForm,
|
||||
AdminServiceMessageLimitForm,
|
||||
AdminServiceRateLimitForm,
|
||||
AdminServiceSMSAllowanceForm,
|
||||
AdminSetEmailBrandingForm,
|
||||
AdminSetOrganizationForm,
|
||||
ChooseEmailBrandingForm,
|
||||
EstimateUsageForm,
|
||||
RenameServiceForm,
|
||||
SearchByNameForm,
|
||||
ServiceContactDetailsForm,
|
||||
@@ -48,10 +55,16 @@ from app.main.forms import (
|
||||
ServiceSmsSenderForm,
|
||||
ServiceSwitchChannelForm,
|
||||
SMSPrefixForm,
|
||||
SomethingElseBrandingForm,
|
||||
)
|
||||
from app.utils import DELIVERED_STATUSES, FAILURE_STATUSES, SENDING_STATUSES
|
||||
from app.utils.branding import get_email_choices as get_email_branding_choices
|
||||
from app.utils.time import parse_naive_dt
|
||||
from app.utils.user import user_has_permissions, user_is_platform_admin
|
||||
from app.utils.user import (
|
||||
user_has_permissions,
|
||||
user_is_gov_user,
|
||||
user_is_platform_admin,
|
||||
)
|
||||
|
||||
PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
|
||||
[
|
||||
@@ -74,6 +87,7 @@ def service_settings(service_id):
|
||||
return render_template(
|
||||
"views/service-settings.html",
|
||||
service_permissions=PLATFORM_ADMIN_SERVICE_PERMISSIONS,
|
||||
email_branding_options=ChooseEmailBrandingForm(current_service),
|
||||
)
|
||||
|
||||
|
||||
@@ -113,6 +127,81 @@ def service_name_change(service_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/request-to-go-live/estimate-usage",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def estimate_usage(service_id):
|
||||
form = EstimateUsageForm(
|
||||
volume_email=current_service.volume_email,
|
||||
volume_sms=current_service.volume_sms,
|
||||
consent_to_research={
|
||||
True: "yes",
|
||||
False: "no",
|
||||
}.get(current_service.consent_to_research),
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_service.update(
|
||||
volume_email=form.volume_email.data,
|
||||
volume_sms=form.volume_sms.data,
|
||||
consent_to_research=(form.consent_to_research.data == "yes"),
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.request_to_go_live",
|
||||
service_id=service_id,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/service-settings/estimate-usage.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/request-to-go-live", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def request_to_go_live(service_id):
|
||||
if current_service.live:
|
||||
return render_template("views/service-settings/service-already-live.html")
|
||||
|
||||
return render_template("views/service-settings/request-to-go-live.html")
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/request-to-go-live", methods=["POST"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_is_gov_user
|
||||
def submit_request_to_go_live(service_id):
|
||||
ticket_message = render_template("support-tickets/go-live-request.txt") + "\n"
|
||||
|
||||
ticket = NotifySupportTicket(
|
||||
subject=f"Request to go live - {current_service.name}",
|
||||
message=ticket_message,
|
||||
ticket_type=NotifySupportTicket.TYPE_QUESTION,
|
||||
user_name=current_user.name,
|
||||
user_email=current_user.email_address,
|
||||
requester_sees_message_content=False,
|
||||
org_id=current_service.organization_id,
|
||||
org_type=current_service.organization_type,
|
||||
service_id=current_service.id,
|
||||
)
|
||||
zendesk_client.send_ticket_to_zendesk(ticket)
|
||||
|
||||
current_service.update(go_live_user=current_user.id)
|
||||
|
||||
flash(
|
||||
"Thanks for your request to go live. We’ll get back to you within one working day.",
|
||||
"default",
|
||||
)
|
||||
return redirect(url_for(".service_settings", service_id=service_id))
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/switch-live", methods=["GET", "POST"]
|
||||
)
|
||||
@@ -124,14 +213,6 @@ def service_switch_live(service_id):
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_service.update_status(live=form.enabled.data)
|
||||
if form.enabled.data is True:
|
||||
billing_api_client.create_or_update_free_sms_fragment_limit(
|
||||
service_id, 250000
|
||||
)
|
||||
else:
|
||||
billing_api_client.create_or_update_free_sms_fragment_limit(
|
||||
service_id, 40000
|
||||
)
|
||||
return redirect(url_for(".service_settings", service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
@@ -795,6 +876,57 @@ def set_rate_limit(service_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/set-email-branding",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def service_set_email_branding(service_id):
|
||||
email_branding = email_branding_client.get_all_email_branding()
|
||||
|
||||
form = AdminSetEmailBrandingForm(
|
||||
all_branding_options=get_branding_as_value_and_label(email_branding),
|
||||
current_branding=current_service.email_branding_id,
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(
|
||||
".service_preview_email_branding",
|
||||
service_id=service_id,
|
||||
branding_style=form.branding_style.data,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/service-settings/set-email-branding.html",
|
||||
form=form,
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/preview-email-branding",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def service_preview_email_branding(service_id):
|
||||
branding_style = request.args.get("branding_style", None)
|
||||
|
||||
form = AdminPreviewBrandingForm(branding_style=branding_style)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_service.update(email_branding=form.branding_style.data)
|
||||
return redirect(url_for(".service_settings", service_id=service_id))
|
||||
|
||||
return render_template(
|
||||
"views/service-settings/preview-email-branding.html",
|
||||
form=form,
|
||||
service_id=service_id,
|
||||
action=url_for("main.service_preview_email_branding", service_id=service_id),
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/link-service-to-organization",
|
||||
methods=["GET", "POST"],
|
||||
@@ -825,6 +957,145 @@ def link_service_to_organization(service_id):
|
||||
)
|
||||
|
||||
|
||||
def create_email_branding_zendesk_ticket(form_option_selected, detail=None):
|
||||
form = ChooseEmailBrandingForm(current_service)
|
||||
|
||||
ticket_message = render_template(
|
||||
"support-tickets/branding-request.txt",
|
||||
current_branding=current_service.email_branding_name,
|
||||
branding_requested=dict(form.options.choices)[form_option_selected],
|
||||
detail=detail,
|
||||
)
|
||||
ticket = NotifySupportTicket(
|
||||
subject=f"Email branding request - {current_service.name}",
|
||||
message=ticket_message,
|
||||
ticket_type=NotifySupportTicket.TYPE_QUESTION,
|
||||
user_name=current_user.name,
|
||||
user_email=current_user.email_address,
|
||||
org_id=current_service.organization_id,
|
||||
org_type=current_service.organization_type,
|
||||
service_id=current_service.id,
|
||||
)
|
||||
zendesk_client.send_ticket_to_zendesk(ticket)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/email-branding",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def email_branding_request(service_id):
|
||||
form = ChooseEmailBrandingForm(current_service)
|
||||
branding_name = current_service.email_branding_name
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(
|
||||
f".email_branding_{form.options.data}",
|
||||
service_id=current_service.id,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/service-settings/branding/email-branding-options.html",
|
||||
form=form,
|
||||
branding_name=branding_name,
|
||||
)
|
||||
|
||||
|
||||
def check_email_branding_allowed_for_service(branding):
|
||||
allowed_branding_for_service = dict(get_email_branding_choices(current_service))
|
||||
|
||||
if branding not in allowed_branding_for_service:
|
||||
abort(404)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/email-branding/govuk",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def email_branding_govuk(service_id):
|
||||
check_email_branding_allowed_for_service("govuk")
|
||||
|
||||
if request.method == "POST":
|
||||
current_service.update(email_branding=None)
|
||||
|
||||
flash("You’ve updated your email branding", "default")
|
||||
return redirect(url_for(".service_settings", service_id=current_service.id))
|
||||
|
||||
return render_template("views/service-settings/branding/email-branding-govuk.html")
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/email-branding/govuk-and-org",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def email_branding_govuk_and_org(service_id):
|
||||
check_email_branding_allowed_for_service("govuk_and_org")
|
||||
|
||||
if request.method == "POST":
|
||||
create_email_branding_zendesk_ticket("govuk_and_org")
|
||||
|
||||
flash(
|
||||
"Thanks for your branding request. We’ll get back to you within one working day.",
|
||||
"default",
|
||||
)
|
||||
return redirect(url_for(".service_settings", service_id=current_service.id))
|
||||
|
||||
return render_template(
|
||||
"views/service-settings/branding/email-branding-govuk-org.html"
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/email-branding/organization",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def email_branding_organization(service_id):
|
||||
check_email_branding_allowed_for_service("organization")
|
||||
|
||||
if request.method == "POST":
|
||||
create_email_branding_zendesk_ticket("organization")
|
||||
|
||||
flash(
|
||||
"Thanks for your branding request. We’ll get back to you within one working day.",
|
||||
"default",
|
||||
)
|
||||
return redirect(url_for(".service_settings", service_id=current_service.id))
|
||||
|
||||
return render_template(
|
||||
"views/service-settings/branding/email-branding-organization.html"
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/email-branding/something-else",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def email_branding_something_else(service_id):
|
||||
form = SomethingElseBrandingForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
create_email_branding_zendesk_ticket(
|
||||
"something_else", detail=form.something_else.data
|
||||
)
|
||||
|
||||
flash(
|
||||
"Thanks for your branding request. We’ll get back to you within one working day.",
|
||||
"default",
|
||||
)
|
||||
return redirect(url_for(".service_settings", service_id=current_service.id))
|
||||
|
||||
return render_template(
|
||||
"views/service-settings/branding/email-branding-something-else.html",
|
||||
form=form,
|
||||
branding_options=ChooseEmailBrandingForm(current_service),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/data-retention", methods=["GET"])
|
||||
@user_is_platform_admin
|
||||
def data_retention(service_id):
|
||||
@@ -913,6 +1184,10 @@ def edit_service_billing_details(service_id):
|
||||
)
|
||||
|
||||
|
||||
def get_branding_as_value_and_label(email_branding):
|
||||
return [(branding["id"], branding["name"]) for branding in email_branding]
|
||||
|
||||
|
||||
def convert_dictionary_to_wtforms_choices_format(dictionary, value, label):
|
||||
return [(item[value], item[label]) for item in dictionary]
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
from flask import (
|
||||
Markup,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
redirect,
|
||||
render_template,
|
||||
@@ -26,100 +21,22 @@ from app.utils import hide_from_search_engines
|
||||
from app.utils.login import is_safe_redirect_url
|
||||
|
||||
|
||||
def _reformat_keystring(orig):
|
||||
new_keystring = orig.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
new_keystring = new_keystring.replace("-----END PRIVATE KEY-----", "")
|
||||
new_keystring = new_keystring.strip()
|
||||
new_keystring = new_keystring.replace(" ", "\n")
|
||||
new_keystring = "\n".join(
|
||||
["-----BEGIN PRIVATE KEY-----", new_keystring, "-----END PRIVATE KEY-----"]
|
||||
)
|
||||
new_keystring = f"{new_keystring}\n"
|
||||
return new_keystring
|
||||
|
||||
|
||||
def _get_access_token(code, state):
|
||||
client_id = os.getenv("LOGIN_DOT_GOV_CLIENT_ID")
|
||||
access_token_url = os.getenv("LOGIN_DOT_GOV_ACCESS_TOKEN_URL")
|
||||
keystring = os.getenv("LOGIN_PEM")
|
||||
if " " in keystring:
|
||||
keystring = _reformat_keystring(keystring)
|
||||
|
||||
payload = {
|
||||
"iss": client_id,
|
||||
"sub": client_id,
|
||||
"aud": access_token_url,
|
||||
"jti": str(uuid.uuid4()),
|
||||
# JWT expiration time (10 minute maximum)
|
||||
"exp": int(time.time()) + (10 * 60),
|
||||
}
|
||||
current_app.logger.warning(f"Here is the raw payload {payload}")
|
||||
token = jwt.encode(payload, keystring, algorithm="RS256")
|
||||
base_url = f"{access_token_url}?"
|
||||
cli_assert = f"client_assertion={token}"
|
||||
cli_assert_type = "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer"
|
||||
code_param = f"code={code}"
|
||||
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
||||
current_app.logger.info(f"This is the url we use to get the access token: {url}")
|
||||
headers = {"Authorization": "Bearer %s" % token}
|
||||
response = requests.post(url, headers=headers)
|
||||
current_app.logger.info(f"GOT A RESPONSE {response.json()}")
|
||||
access_token = response.json()["access_token"]
|
||||
return access_token
|
||||
|
||||
|
||||
def _get_user_email(access_token):
|
||||
headers = {"Authorization": "Bearer %s" % access_token}
|
||||
user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL")
|
||||
user_attributes = requests.get(
|
||||
user_info_url,
|
||||
headers=headers,
|
||||
)
|
||||
user_email = user_attributes.json()["email"]
|
||||
return user_email
|
||||
|
||||
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in():
|
||||
# start login.gov
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
login_gov_error = request.args.get("error")
|
||||
if code and state:
|
||||
access_token = _get_access_token(code, state)
|
||||
user_email = _get_user_email(access_token)
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
# activate the user
|
||||
user = user_api_client.get_user_by_email(user_email)
|
||||
activate_user(user["id"])
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
|
||||
elif login_gov_error:
|
||||
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||
# end login.gov
|
||||
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
current_app.logger.warning("E2E TESTS ARE ENABLED.")
|
||||
current_app.logger.warning(
|
||||
"If you are getting a 404 on signin, comment out E2E vars in .env file!"
|
||||
)
|
||||
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
|
||||
activate_user(user["id"])
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
|
||||
current_app.logger.info(f"current user is {current_user}")
|
||||
if current_user and current_user.is_authenticated:
|
||||
if redirect_url and is_safe_redirect_url(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
|
||||
form = LoginForm()
|
||||
current_app.logger.info("Got the login form")
|
||||
password_reset_url = url_for(".forgot_password", next=request.args.get("next"))
|
||||
|
||||
if form.validate_on_submit():
|
||||
@@ -167,40 +84,12 @@ def sign_in():
|
||||
)
|
||||
|
||||
other_device = current_user.logged_in_elsewhere()
|
||||
notify_env = os.getenv("NOTIFY_ENVIRONMENT")
|
||||
current_app.logger.info("should render the sign in template")
|
||||
|
||||
# TODO REMOVE THIS INFO ONCE STAGING WORKS WITH LOGIN DOT GOV
|
||||
current_app.logger.info(f"NOTIFY ENV = {notify_env}")
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_CLIENT_ID={os.getenv('LOGIN_DOT_GOV_CLIENT_ID')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_USER_INFO_URL={os.getenv('LOGIN_DOT_GOV_USER_INFO_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_ACCESS_TOKEN_URL={os.getenv('LOGIN_DOT_GOV_ACCESS_TOKEN_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_LOGOUT_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_BASE_LOGOUT_URL={os.getenv('LOGIN_DOT_GOV_BASE_LOGOUT_URL')}"
|
||||
)
|
||||
current_app.logger.info(
|
||||
f"LOGIN_DOT_GOV_SIGNOUT_REDIRECT={os.getenv('LOGIN_DOT_GOV_SIGNOUT_REDIRECT')}"
|
||||
)
|
||||
initial_signin_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
current_app.logger.info(f"LOGIN_DOT_GOV_INITIAL_SIGNIN_URL={initial_signin_url}")
|
||||
|
||||
return render_template(
|
||||
"views/signin.html",
|
||||
form=form,
|
||||
again=bool(redirect_url),
|
||||
other_device=other_device,
|
||||
login_gov_enabled=bool(notify_env in ["development", "staging"]),
|
||||
password_reset_url=password_reset_url,
|
||||
initial_signin_url=initial_signin_url,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +1,12 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
from flask import current_app, redirect, url_for
|
||||
from flask import redirect, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app.main import main
|
||||
|
||||
# ask login.gov if we really need manual logout and what's up with one hour sessions
|
||||
# ask login.gov how they recommend approaching dev environment
|
||||
# ask Tim Donaworth the same for #2
|
||||
|
||||
|
||||
def _sign_out_at_login_dot_gov():
|
||||
base_url = os.getenv("LOGIN_DOT_GOV_BASE_LOGOUT_URL")
|
||||
client_id = f"client_id={os.getenv('LOGIN_DOT_GOV_CLIENT_ID')}"
|
||||
post_logout_redirect_uri = (
|
||||
f"post_logout_redirect_uri={os.getenv('LOGIN_DOT_GOV_SIGNOUT_REDIRECT')}"
|
||||
)
|
||||
|
||||
url = f"{base_url}{client_id}&{post_logout_redirect_uri}"
|
||||
current_app.logger.info(f"url={url}")
|
||||
|
||||
response = requests.post(url)
|
||||
|
||||
# response = requests.post(url)
|
||||
current_app.logger.info(f"login.gov response: {response.text}")
|
||||
|
||||
|
||||
@main.route("/sign-out", methods=(["GET", "POST"]))
|
||||
@main.route("/sign-out", methods=(["GET"]))
|
||||
def sign_out():
|
||||
# An AnonymousUser does not have an id
|
||||
current_app.logger.info("HIT THE REGULAR SIGN OUT")
|
||||
if current_user.is_authenticated:
|
||||
# TODO This doesn't work yet, due to problems above.
|
||||
current_user.sign_out()
|
||||
login_dot_gov_logout_url = os.getenv("LOGIN_DOT_GOV_LOGOUT_URL")
|
||||
if login_dot_gov_logout_url:
|
||||
return redirect(login_dot_gov_logout_url)
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
@@ -4,6 +4,10 @@ def features_nav():
|
||||
"name": "Features",
|
||||
"link": "main.features",
|
||||
"sub_navigation_items": [
|
||||
# {
|
||||
# "name": "Emails",
|
||||
# "link": "main.features_email",
|
||||
# },
|
||||
# {
|
||||
# "name": "Text messages",
|
||||
# "link": "main.features_sms",
|
||||
@@ -52,6 +56,10 @@ def using_notify_nav():
|
||||
# "link": "main.edit_and_format_messages",
|
||||
# },
|
||||
# {
|
||||
# "name": "Branding",
|
||||
# "link": "main.branding_and_customisation",
|
||||
# },
|
||||
# {
|
||||
# "name": "Send files by email",
|
||||
# "link": "main.send_files_by_email",
|
||||
# },
|
||||
|
||||
@@ -2,7 +2,6 @@ from functools import partial
|
||||
|
||||
from flask import abort, flash, jsonify, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
||||
|
||||
@@ -680,23 +679,8 @@ def count_content_length(service_id, template_type):
|
||||
)
|
||||
|
||||
|
||||
def _is_latin1(s):
|
||||
return bool(s.encode(encoding="latin-1", errors="strict"))
|
||||
|
||||
|
||||
def _get_content_count_error_and_message_for_template(template):
|
||||
url = "https://en.wikipedia.org/wiki/ISO/IEC_8859-1"
|
||||
if template.template_type == "sms":
|
||||
s1 = f"<html><body>Use of characters outside the <a href='{url}'>IEC_8859-1</a> character set may increase "
|
||||
s2 = "the message fragment count, resulting in additional charges, and these IEC_8859-1 "
|
||||
s3 = "characters may not display properly on some older mobile devices.</body></html>"
|
||||
|
||||
warning = ""
|
||||
try:
|
||||
_is_latin1(template.content)
|
||||
except UnicodeEncodeError:
|
||||
warning = f"{s1}{s2}{s3}"
|
||||
|
||||
if template.is_message_too_long():
|
||||
return True, (
|
||||
f"You have "
|
||||
@@ -705,16 +689,11 @@ def _get_content_count_error_and_message_for_template(template):
|
||||
)
|
||||
if template.placeholders:
|
||||
return False, (
|
||||
Markup(
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
f"(not including personalization). {warning}"
|
||||
)
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
f"(not including personalization)"
|
||||
)
|
||||
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} "
|
||||
)
|
||||
f"Will be charged as {message_count(template.fragment_count, template.template_type)} "
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ from app.main.forms import (
|
||||
ChangeMobileNumberForm,
|
||||
ChangeNameForm,
|
||||
ChangePasswordForm,
|
||||
ChangePreferredTimezoneForm,
|
||||
ConfirmPasswordForm,
|
||||
ServiceOnOffSettingForm,
|
||||
TwoFactorForm,
|
||||
@@ -62,22 +61,6 @@ def user_profile_name():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/preferred_timezone", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_preferred_timezone():
|
||||
form = ChangePreferredTimezoneForm(new_name=current_user.preferred_timezone)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_user.update(preferred_timezone=form.new_preferred_timezone.data)
|
||||
current_user.preferred_timezone = form.new_preferred_timezone.data
|
||||
return redirect(url_for(".user_profile"))
|
||||
return render_template(
|
||||
"views/user-profile/change.html",
|
||||
thing="preferred timezone",
|
||||
form_field=form.new_preferred_timezone,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/user-profile/email", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
@user_is_gov_user
|
||||
|
||||
@@ -66,6 +66,9 @@ class ServiceEvent(Event):
|
||||
def format_contact_link(self):
|
||||
return "Set the contact details for this service to ‘{}’".format(self.value_to)
|
||||
|
||||
def format_email_branding(self):
|
||||
return "Updated this service’s email branding"
|
||||
|
||||
def format_inbound_api(self):
|
||||
return "Updated the callback for received text messages"
|
||||
|
||||
@@ -113,6 +116,9 @@ class ServiceEvent(Event):
|
||||
def format_service_callback_api(self):
|
||||
return "Updated the callback for delivery receipts"
|
||||
|
||||
def format_go_live_user(self):
|
||||
return "Requested for this service to go live"
|
||||
|
||||
|
||||
class APIKeyEvent(Event):
|
||||
relevant = True
|
||||
|
||||
3
app/models/feedback.py
Normal file
3
app/models/feedback.py
Normal file
@@ -0,0 +1,3 @@
|
||||
QUESTION_TICKET_TYPE = "ask-question-give-feedback"
|
||||
PROBLEM_TICKET_TYPE = "report-problem"
|
||||
GENERAL_TICKET_TYPE = "general"
|
||||
@@ -3,6 +3,7 @@ from collections import OrderedDict
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.models import JSONModel, ModelList, SerialisedModelCollection, SortByNameMixin
|
||||
from app.notify_client.email_branding_client import email_branding_client
|
||||
from app.notify_client.organizations_api_client import organizations_client
|
||||
|
||||
|
||||
@@ -24,7 +25,9 @@ class Organization(JSONModel, SortByNameMixin):
|
||||
"name",
|
||||
"active",
|
||||
"organization_type",
|
||||
"email_branding_id",
|
||||
"domains",
|
||||
"request_to_go_live_notes",
|
||||
"count_of_live_services",
|
||||
"billing_contact_email_addresses",
|
||||
"billing_contact_names",
|
||||
@@ -70,6 +73,8 @@ class Organization(JSONModel, SortByNameMixin):
|
||||
self.name = None
|
||||
self.domains = []
|
||||
self.organization_type = None
|
||||
self.request_to_go_live_notes = None
|
||||
self.email_branding_id = None
|
||||
|
||||
@property
|
||||
def organization_type_label(self):
|
||||
@@ -123,6 +128,19 @@ class Organization(JSONModel, SortByNameMixin):
|
||||
key=lambda user: user.email_address.lower(),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def email_branding(self):
|
||||
if self.email_branding_id:
|
||||
return email_branding_client.get_email_branding(self.email_branding_id)[
|
||||
"email_branding"
|
||||
]
|
||||
|
||||
@property
|
||||
def email_branding_name(self):
|
||||
if self.email_branding_id:
|
||||
return self.email_branding["name"]
|
||||
return "GOV.UK"
|
||||
|
||||
def update(self, delete_services_cache=False, **kwargs):
|
||||
response = organizations_client.update_organization(
|
||||
self.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ from app.models.organization import Organization
|
||||
from app.models.user import InvitedUsers, User, Users
|
||||
from app.notify_client.api_key_api_client import api_key_api_client
|
||||
from app.notify_client.billing_api_client import billing_api_client
|
||||
from app.notify_client.email_branding_client import email_branding_client
|
||||
from app.notify_client.inbound_number_client import inbound_number_client
|
||||
from app.notify_client.invite_api_client import invite_api_client
|
||||
from app.notify_client.job_api_client import job_api_client
|
||||
@@ -190,15 +191,6 @@ class Service(JSONModel, SortByNameMixin):
|
||||
invited_user_id=str(invited_user_id),
|
||||
)
|
||||
|
||||
def resend_invite(self, invited_user_id):
|
||||
if str(invited_user_id) not in {user.id for user in self.invited_users}:
|
||||
abort(404)
|
||||
|
||||
return invite_api_client.resend_invite(
|
||||
service_id=self.id,
|
||||
invited_user_id=str(invited_user_id),
|
||||
)
|
||||
|
||||
def get_team_member(self, user_id):
|
||||
if str(user_id) not in {user.id for user in self.active_users}:
|
||||
abort(404)
|
||||
@@ -379,6 +371,22 @@ class Service(JSONModel, SortByNameMixin):
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def go_live_checklist_completed(self):
|
||||
return all(
|
||||
(
|
||||
bool(self.volumes),
|
||||
self.has_team_members,
|
||||
self.has_templates,
|
||||
not self.needs_to_add_email_reply_to_address,
|
||||
not self.needs_to_change_sms_sender,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def go_live_checklist_completed_as_yes_no(self):
|
||||
return "Yes" if self.go_live_checklist_completed else "No"
|
||||
|
||||
@cached_property
|
||||
def free_sms_fragment_limit(self):
|
||||
return billing_api_client.get_free_sms_fragment_limit_for_year(self.id) or 0
|
||||
@@ -400,6 +408,31 @@ class Service(JSONModel, SortByNameMixin):
|
||||
{},
|
||||
).get("days_of_retention", current_app.config["ACTIVITY_STATS_LIMIT_DAYS"])
|
||||
|
||||
@property
|
||||
def email_branding_id(self):
|
||||
return self._dict["email_branding"]
|
||||
|
||||
@cached_property
|
||||
def email_branding(self):
|
||||
if self.email_branding_id:
|
||||
return email_branding_client.get_email_branding(self.email_branding_id)[
|
||||
"email_branding"
|
||||
]
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def email_branding_name(self):
|
||||
if self.email_branding is None:
|
||||
return "GOV.UK"
|
||||
return self.email_branding["name"]
|
||||
|
||||
@property
|
||||
def needs_to_change_email_branding(self):
|
||||
return (
|
||||
self.email_branding_id is None
|
||||
and self.organization_type != Organization.TYPE_CENTRAL
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def organization(self):
|
||||
return Organization.from_id(self.organization_id)
|
||||
|
||||
@@ -47,7 +47,6 @@ class User(JSONModel, UserMixin):
|
||||
"password_changed_at",
|
||||
"permissions",
|
||||
"state",
|
||||
"preferred_timezone",
|
||||
}
|
||||
|
||||
def __init__(self, _dict):
|
||||
@@ -365,7 +364,6 @@ class User(JSONModel, UserMixin):
|
||||
"permissions": [x for x in self._permissions],
|
||||
"organizations": self.organization_ids,
|
||||
"current_session_id": self.current_session_id,
|
||||
"preferred_timezone": self.preferred_timezone,
|
||||
}
|
||||
if hasattr(self, "_password"):
|
||||
dct["password"] = self._password
|
||||
|
||||
@@ -38,10 +38,16 @@ class Navigation:
|
||||
class HeaderNavigation(Navigation):
|
||||
mapping = {
|
||||
"support": {
|
||||
"bat_phone",
|
||||
"feedback",
|
||||
"support",
|
||||
"support_public",
|
||||
"thanks",
|
||||
"triage",
|
||||
},
|
||||
"features": {
|
||||
"features",
|
||||
"features_email",
|
||||
"features_sms",
|
||||
"roadmap",
|
||||
"security",
|
||||
@@ -87,7 +93,49 @@ class HeaderNavigation(Navigation):
|
||||
"uploads",
|
||||
"view_job",
|
||||
"view_jobs",
|
||||
"confirm_edit_user_email",
|
||||
"confirm_edit_user_mobile_number",
|
||||
"edit_user_email",
|
||||
"edit_user_mobile_number",
|
||||
"edit_user_permissions",
|
||||
"invite_user",
|
||||
"manage_users",
|
||||
"remove_user_from_service",
|
||||
"usage",
|
||||
"email_branding_govuk",
|
||||
"email_branding_govuk_and_org",
|
||||
"email_branding_organization",
|
||||
"email_branding_request",
|
||||
"email_branding_something_else",
|
||||
"estimate_usage",
|
||||
"link_service_to_organization",
|
||||
"request_to_go_live",
|
||||
"service_add_email_reply_to",
|
||||
"service_add_sms_sender",
|
||||
"service_confirm_delete_email_reply_to",
|
||||
"service_confirm_delete_sms_sender",
|
||||
"service_edit_email_reply_to",
|
||||
"service_edit_sms_sender",
|
||||
"service_email_reply_to",
|
||||
"service_name_change",
|
||||
"service_preview_email_branding",
|
||||
"service_set_auth_type",
|
||||
"service_set_channel",
|
||||
"send_files_by_email_contact_details",
|
||||
"service_set_email_branding",
|
||||
"service_set_inbound_number",
|
||||
"service_set_inbound_sms",
|
||||
"service_set_international_sms",
|
||||
"service_set_reply_to_email",
|
||||
"service_set_sms_prefix",
|
||||
"service_verify_reply_to_address",
|
||||
"service_verify_reply_to_address_updates",
|
||||
"service_settings",
|
||||
"service_sms_senders",
|
||||
"set_free_sms_allowance",
|
||||
"set_message_limit",
|
||||
"set_rate_limit",
|
||||
"submit_request_to_go_live",
|
||||
},
|
||||
"pricing": {
|
||||
"how_to_pay",
|
||||
@@ -109,13 +157,15 @@ class HeaderNavigation(Navigation):
|
||||
"user_profile_mobile_number_delete",
|
||||
"user_profile_name",
|
||||
"user_profile_password",
|
||||
"user_profile_preferred_timezone",
|
||||
"user_profile_disable_platform_admin_view",
|
||||
},
|
||||
"platform-admin": {
|
||||
"archive_user",
|
||||
"change_user_auth",
|
||||
"clear_cache",
|
||||
"create_email_branding",
|
||||
"edit_sms_provider_ratio",
|
||||
"email_branding",
|
||||
"find_services_by_name",
|
||||
"find_users_by_email",
|
||||
"live_services",
|
||||
@@ -133,7 +183,10 @@ class HeaderNavigation(Navigation):
|
||||
"platform_admin_splash_page",
|
||||
"suspend_service",
|
||||
"trial_services",
|
||||
"update_email_branding",
|
||||
"user_information",
|
||||
"view_provider",
|
||||
"view_providers",
|
||||
},
|
||||
"sign-in": {
|
||||
"revalidate_email_sent",
|
||||
@@ -204,23 +257,16 @@ class MainNavigation(Navigation):
|
||||
"usage": {
|
||||
"usage",
|
||||
},
|
||||
"user-profile": {
|
||||
"user_profile",
|
||||
"user_profile_confirm_delete_mobile_number",
|
||||
"user_profile_email",
|
||||
"user_profile_email_authenticate",
|
||||
"user_profile_email_confirm",
|
||||
"user_profile_mobile_number",
|
||||
"user_profile_mobile_number_authenticate",
|
||||
"user_profile_mobile_number_confirm",
|
||||
"user_profile_mobile_number_delete",
|
||||
"user_profile_name",
|
||||
"user_profile_password",
|
||||
"user_profile_preferred_timezone",
|
||||
"user_profile_disable_platform_admin_view",
|
||||
},
|
||||
"settings": {
|
||||
# 'add_organization_from_gp_service',
|
||||
"email_branding_govuk",
|
||||
"email_branding_govuk_and_org",
|
||||
"email_branding_organization",
|
||||
"email_branding_request",
|
||||
"email_branding_something_else",
|
||||
"estimate_usage",
|
||||
"link_service_to_organization",
|
||||
"request_to_go_live",
|
||||
"service_add_email_reply_to",
|
||||
"service_add_sms_sender",
|
||||
"service_confirm_delete_email_reply_to",
|
||||
@@ -229,9 +275,11 @@ class MainNavigation(Navigation):
|
||||
"service_edit_sms_sender",
|
||||
"service_email_reply_to",
|
||||
"service_name_change",
|
||||
"service_preview_email_branding",
|
||||
"service_set_auth_type",
|
||||
"service_set_channel",
|
||||
"send_files_by_email_contact_details",
|
||||
"service_set_email_branding",
|
||||
"service_set_inbound_number",
|
||||
"service_set_inbound_sms",
|
||||
"service_set_international_sms",
|
||||
@@ -244,6 +292,7 @@ class MainNavigation(Navigation):
|
||||
"set_free_sms_allowance",
|
||||
"set_message_limit",
|
||||
"set_rate_limit",
|
||||
"submit_request_to_go_live",
|
||||
},
|
||||
"api-integration": {
|
||||
"api_callbacks",
|
||||
@@ -280,58 +329,6 @@ class CaseworkNavigation(Navigation):
|
||||
}
|
||||
|
||||
|
||||
class SecondaryNavigation(Navigation):
|
||||
mapping = {
|
||||
"settings": {
|
||||
"link_service_to_organization",
|
||||
"service_add_email_reply_to",
|
||||
"service_add_sms_sender",
|
||||
"service_confirm_delete_email_reply_to",
|
||||
"service_confirm_delete_sms_sender",
|
||||
"service_edit_email_reply_to",
|
||||
"service_edit_sms_sender",
|
||||
"service_email_reply_to",
|
||||
"service_name_change",
|
||||
"service_set_auth_type",
|
||||
"service_set_channel",
|
||||
"send_files_by_email_contact_details",
|
||||
"service_set_inbound_number",
|
||||
"service_set_inbound_sms",
|
||||
"service_set_international_sms",
|
||||
"service_set_reply_to_email",
|
||||
"service_set_sms_prefix",
|
||||
"service_verify_reply_to_address",
|
||||
"service_verify_reply_to_address_updates",
|
||||
"service_settings",
|
||||
"service_sms_senders",
|
||||
"set_free_sms_allowance",
|
||||
"set_message_limit",
|
||||
"set_rate_limit",
|
||||
"confirm_edit_user_email",
|
||||
"confirm_edit_user_mobile_number",
|
||||
"edit_user_email",
|
||||
"edit_user_mobile_number",
|
||||
"edit_user_permissions",
|
||||
"invite_user",
|
||||
"manage_users",
|
||||
"remove_user_from_service",
|
||||
"user_profile",
|
||||
"user_profile_confirm_delete_mobile_number",
|
||||
"user_profile_email",
|
||||
"user_profile_email_authenticate",
|
||||
"user_profile_email_confirm",
|
||||
"user_profile_mobile_number",
|
||||
"user_profile_mobile_number_authenticate",
|
||||
"user_profile_mobile_number_confirm",
|
||||
"user_profile_mobile_number_delete",
|
||||
"user_profile_name",
|
||||
"user_profile_password",
|
||||
"user_profile_preferred_timezone",
|
||||
"user_profile_disable_platform_admin_view",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OrgNavigation(Navigation):
|
||||
mapping = {
|
||||
"dashboard": {
|
||||
@@ -340,9 +337,12 @@ class OrgNavigation(Navigation):
|
||||
"settings": {
|
||||
"edit_organization_billing_details",
|
||||
"edit_organization_domains",
|
||||
"edit_organization_email_branding",
|
||||
"edit_organization_go_live_notes",
|
||||
"edit_organization_name",
|
||||
"edit_organization_notes",
|
||||
"edit_organization_type",
|
||||
"organization_preview_email_branding",
|
||||
"organization_settings",
|
||||
},
|
||||
"team-members": {
|
||||
|
||||
40
app/notify_client/email_branding_client.py
Normal file
40
app/notify_client/email_branding_client.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from app.notify_client import NotifyAdminAPIClient, cache
|
||||
|
||||
|
||||
class EmailBrandingClient(NotifyAdminAPIClient):
|
||||
@cache.set("email_branding-{branding_id}")
|
||||
def get_email_branding(self, branding_id):
|
||||
return self.get(url="/email-branding/{}".format(branding_id))
|
||||
|
||||
@cache.set("email_branding")
|
||||
def get_all_email_branding(self, sort_key=None):
|
||||
brandings = self.get(url="/email-branding")["email_branding"]
|
||||
if sort_key and sort_key in brandings[0]:
|
||||
brandings.sort(key=lambda branding: branding[sort_key].lower())
|
||||
return brandings
|
||||
|
||||
@cache.delete("email_branding")
|
||||
def create_email_branding(self, logo, name, text, colour, brand_type):
|
||||
data = {
|
||||
"logo": logo,
|
||||
"name": name,
|
||||
"text": text,
|
||||
"colour": colour,
|
||||
"brand_type": brand_type,
|
||||
}
|
||||
return self.post(url="/email-branding", data=data)
|
||||
|
||||
@cache.delete("email_branding")
|
||||
@cache.delete("email_branding-{branding_id}")
|
||||
def update_email_branding(self, branding_id, logo, name, text, colour, brand_type):
|
||||
data = {
|
||||
"logo": logo,
|
||||
"name": name,
|
||||
"text": text,
|
||||
"colour": colour,
|
||||
"brand_type": brand_type,
|
||||
}
|
||||
return self.post(url="/email-branding/{}".format(branding_id), data=data)
|
||||
|
||||
|
||||
email_branding_client = EmailBrandingClient()
|
||||
@@ -32,11 +32,11 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
"folder_permissions": folder_permissions,
|
||||
}
|
||||
data = _attach_current_user(data)
|
||||
resp = self.post(url=f"/service/{service_id}/invite", data=data)
|
||||
resp = self.post(url="/service/{}/invite".format(service_id), data=data)
|
||||
return resp["data"]
|
||||
|
||||
def get_invites_for_service(self, service_id):
|
||||
return self.get(f"/service/{service_id}/invite")["data"]
|
||||
return self.get("/service/{}/invite".format(service_id))["data"]
|
||||
|
||||
def get_invited_user(self, invited_user_id):
|
||||
return self.get(f"/invite/service/{invited_user_id}")["data"]
|
||||
@@ -46,7 +46,7 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
|
||||
def get_count_of_invites_with_permission(self, service_id, permission):
|
||||
if permission not in all_ui_permissions:
|
||||
raise TypeError(f"{permission} is not a valid permission")
|
||||
raise TypeError("{} is not a valid permission".format(permission))
|
||||
return len(
|
||||
[
|
||||
invited_user
|
||||
@@ -56,21 +56,22 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
)
|
||||
|
||||
def check_token(self, token):
|
||||
return self.get(url=f"/invite/service/check/{token}")["data"]
|
||||
return self.get(url="/invite/service/check/{}".format(token))["data"]
|
||||
|
||||
def cancel_invited_user(self, service_id, invited_user_id):
|
||||
data = {"status": "cancelled"}
|
||||
data = _attach_current_user(data)
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
|
||||
|
||||
def resend_invite(self, service_id, invited_user_id):
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}/resend", data={})
|
||||
self.post(
|
||||
url="/service/{0}/invite/{1}".format(service_id, invited_user_id), data=data
|
||||
)
|
||||
|
||||
@cache.delete("service-{service_id}")
|
||||
@cache.delete("user-{invited_user_id}")
|
||||
def accept_invite(self, service_id, invited_user_id):
|
||||
data = {"status": "accepted"}
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
|
||||
self.post(
|
||||
url="/service/{0}/invite/{1}".format(service_id, invited_user_id), data=data
|
||||
)
|
||||
|
||||
|
||||
invite_api_client = InviteApiClient()
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from app.extensions import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
|
||||
|
||||
class JobApiClient(NotifyAdminAPIClient):
|
||||
@@ -90,45 +86,11 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
def has_jobs(self, service_id):
|
||||
return bool(self.get_jobs(service_id)["data"])
|
||||
|
||||
@classmethod
|
||||
def convert_user_time_to_utc(cls, scheduled_for):
|
||||
user_preferred_tz = get_user_preferred_timezone()
|
||||
|
||||
user_date = datetime.datetime.fromisoformat(scheduled_for)
|
||||
scheduled_for = (
|
||||
user_date.replace(tzinfo=ZoneInfo(user_preferred_tz))
|
||||
.astimezone(ZoneInfo("UTC"))
|
||||
.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
)
|
||||
|
||||
return scheduled_for
|
||||
|
||||
def create_job(
|
||||
self,
|
||||
job_id,
|
||||
service_id,
|
||||
scheduled_for=None,
|
||||
template_id=None,
|
||||
original_file_name=None,
|
||||
notification_count=None,
|
||||
valid=None,
|
||||
):
|
||||
def create_job(self, job_id, service_id, scheduled_for=None):
|
||||
data = {"id": job_id}
|
||||
|
||||
# make a datetime object in the user's preferred timezone
|
||||
|
||||
if scheduled_for:
|
||||
scheduled_for = JobApiClient.convert_user_time_to_utc(scheduled_for)
|
||||
data["scheduled_for"] = scheduled_for
|
||||
|
||||
if template_id:
|
||||
data["template_id"] = template_id
|
||||
if original_file_name:
|
||||
data["original_file_name"] = original_file_name
|
||||
if notification_count:
|
||||
data["notification_count"] = notification_count
|
||||
if valid:
|
||||
data["valid"] = valid
|
||||
data.update({"scheduled_for": scheduled_for})
|
||||
|
||||
data = _attach_current_user(data)
|
||||
job = self.post(url="/service/{}/job".format(service_id), data=data)
|
||||
|
||||
20
app/notify_client/provider_client.py
Normal file
20
app/notify_client/provider_client.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
||||
|
||||
|
||||
class ProviderClient(NotifyAdminAPIClient):
|
||||
def get_all_providers(self):
|
||||
return self.get(url="/provider-details")
|
||||
|
||||
def get_provider_by_id(self, provider_id):
|
||||
return self.get(url="/provider-details/{}".format(provider_id))
|
||||
|
||||
def get_provider_versions(self, provider_id):
|
||||
return self.get(url="/provider-details/{}/versions".format(provider_id))
|
||||
|
||||
def update_provider(self, provider_id, priority):
|
||||
data = {"priority": priority}
|
||||
data = _attach_current_user(data)
|
||||
return self.post(url="/provider-details/{}".format(provider_id), data=data)
|
||||
|
||||
|
||||
provider_client = ProviderClient()
|
||||
@@ -82,6 +82,7 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
"contact_link",
|
||||
"created_by",
|
||||
"count_as_live",
|
||||
"email_branding",
|
||||
"email_from",
|
||||
"free_sms_fragment_limit",
|
||||
"go_live_at",
|
||||
|
||||
@@ -3,7 +3,7 @@ from app.notify_client import NotifyAdminAPIClient, cache
|
||||
|
||||
class StatusApiClient(NotifyAdminAPIClient):
|
||||
def get_status(self, *params):
|
||||
return self.get(*params, url="/_status")
|
||||
return self.get(url="/_status", *params)
|
||||
|
||||
@cache.set("live-service-and-organization-counts", ttl_in_seconds=3600)
|
||||
def get_count_of_live_services_and_organizations(self):
|
||||
|
||||
@@ -12,7 +12,6 @@ ALLOWED_ATTRIBUTES = {
|
||||
"updated_by",
|
||||
"current_session_id",
|
||||
"email_access_validated_at",
|
||||
"preferred_timezone",
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +39,6 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
return self.get("/user/{}".format(user_id))
|
||||
|
||||
def get_user_by_email(self, email_address):
|
||||
current_app.logger.info(f"Going to get user by email {email_address}")
|
||||
user_data = self.post("/user/email", data={"email": email_address})
|
||||
return user_data["data"]
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
|
||||
import pytz
|
||||
from dateutil import parser
|
||||
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
|
||||
|
||||
def sum_of_statistics(delivery_statistics):
|
||||
statistics_keys = (
|
||||
@@ -27,7 +24,6 @@ def sum_of_statistics(delivery_statistics):
|
||||
|
||||
|
||||
def add_rates_to(delivery_statistics):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return dict(
|
||||
emails_failure_rate=get_formatted_percentage(
|
||||
delivery_statistics["emails_failed"],
|
||||
@@ -37,7 +33,7 @@ def add_rates_to(delivery_statistics):
|
||||
delivery_statistics["sms_failed"], delivery_statistics["sms_requested"]
|
||||
),
|
||||
week_end_datetime=parser.parse(
|
||||
delivery_statistics.get("week_end", str(datetime.now(preferred_tz)))
|
||||
delivery_statistics.get("week_end", str(datetime.utcnow()))
|
||||
),
|
||||
**delivery_statistics
|
||||
)
|
||||
|
||||
@@ -58,37 +58,26 @@
|
||||
"text": "Features",
|
||||
"active": header_navigation.is_selected('features')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.user_profile'),
|
||||
"text": current_user.name,
|
||||
"active": header_navigation.is_selected('user-profile')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.platform_admin_splash_page'),
|
||||
"text": "Platform admin",
|
||||
"active": header_navigation.is_selected('platform-admin')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
},
|
||||
{
|
||||
"href": url_for('main.support'),
|
||||
"text": "Contact us",
|
||||
"active": header_navigation.is_selected('support')
|
||||
}
|
||||
] %}
|
||||
{% if current_service %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.service_settings', service_id=current_service.id),
|
||||
"text": "Settings",
|
||||
"active": secondary_navigation.is_selected('settings')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set navigation = [
|
||||
{
|
||||
@@ -115,29 +104,13 @@
|
||||
"href": url_for('main.user_profile'),
|
||||
"text": "User profile",
|
||||
"active": header_navigation.is_selected('user-profile')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% if current_service %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.service_settings', service_id=current_service.id),
|
||||
"text": "Settings",
|
||||
"active": secondary_navigation.is_selected('settings')
|
||||
},
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{
|
||||
"href": url_for('main.sign_out'),
|
||||
"text": "Sign out"
|
||||
}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- Add navigation back after pilot -->
|
||||
{# {% set navigation = [
|
||||
@@ -169,7 +142,6 @@
|
||||
"productName": "Notify",
|
||||
"navigation": navigation,
|
||||
"navigationClasses": "govuk-header__navigation--end",
|
||||
"secondaryNavigation": secondaryNavigation,
|
||||
"assetsPath": asset_path + "images"
|
||||
}) }}
|
||||
{% endblock %}
|
||||
@@ -264,7 +236,7 @@
|
||||
<span class="usa-sr-only">Please choose to extend your session or sign out. Your session will expire in 5 minutes or less.</span>
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p>You have been inactive for too long.
|
||||
<p>You have been inactive for too long.
|
||||
Your session will expire in <span id="timeLeft" role="timer"></span>.
|
||||
</p>
|
||||
</div>
|
||||
@@ -301,3 +273,5 @@
|
||||
<!--<![endif]-->
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,54 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro big_number_with_status(
|
||||
number,
|
||||
label,
|
||||
failures,
|
||||
failure_percentage,
|
||||
danger_zone=False,
|
||||
failure_link=None,
|
||||
link=None,
|
||||
show_failures=True,
|
||||
smaller=False,
|
||||
smallest=False
|
||||
) %}
|
||||
<span class="big-number-with-status">
|
||||
{{ big_number(number, label, link=link, smaller=smaller, smallest=smallest) }}
|
||||
{% if show_failures %}
|
||||
<span class="big-number-status{% if danger_zone %} big-number-status--failing{% endif %}">
|
||||
{% if failures %}
|
||||
{% if failure_link %}
|
||||
<a class="usa-link" href="{{ failure_link }}">
|
||||
{{ "{:,}".format(failures) }}
|
||||
failed – {{ failure_percentage }}%
|
||||
</a>
|
||||
{% else %}
|
||||
{{ "{:,}".format(failures) }}
|
||||
failed – {{ failure_percentage }}%
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No failures
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro big_number_simple(number, label) %}
|
||||
<span class="big-number-dark bottom-gutter-2-3">
|
||||
<span class="big-number-number">
|
||||
{% if number is number %}
|
||||
{{ "{:,}".format(number) }}
|
||||
{% else %}
|
||||
{{ number }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if label %}
|
||||
<span class="big-number-label">{{ label }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
7
app/templates/components/branding-preview.html
Normal file
7
app/templates/components/branding-preview.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% macro branding_preview(branding_style, endpoint) %}
|
||||
<iframe src="{{ url_for(endpoint, branding_style=branding_style) }}" class="branding-preview" scrolling="no"></iframe>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro email_branding_preview(branding_style) %}
|
||||
{{ branding_preview(branding_style, 'main.email_template') }}
|
||||
{% endmacro %}
|
||||
15
app/templates/components/components/alert/README.md
Normal file
15
app/templates/components/components/alert/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Error message
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the error message component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/error-message).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
|
||||
See [options table](https://design-system.service.gov.uk/components/error-message/#options-example-default) for details.
|
||||
37
app/templates/components/components/alert/macro-options.json
Normal file
37
app/templates/components/components/alert/macro-options.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "If `html` is set, this is not required. Text to use within the error message. If `html` is provided, the `text` argument will be ignored."
|
||||
},
|
||||
{
|
||||
"name": "html",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "If `text` is set, this is not required. HTML to use within the error message. If `html` is provided, the `text` argument will be ignored."
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Id attribute to add to the error message span tag."
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the error message span tag."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the error message span tag"
|
||||
},
|
||||
{
|
||||
"name": "visuallyHiddenText",
|
||||
"type": "string",
|
||||
"description": "A visually hidden prefix used before the error message. Defaults to \"Error\"."
|
||||
}
|
||||
]
|
||||
3
app/templates/components/components/alert/macro.njk
Normal file
3
app/templates/components/components/alert/macro.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro usaAlert(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
18
app/templates/components/components/alert/template.njk
Normal file
18
app/templates/components/components/alert/template.njk
Normal file
@@ -0,0 +1,18 @@
|
||||
{%- if params.slim %}
|
||||
<div class="usa-alert usa-alert--slim usa-alert--{{params.type}} {%- if params.classes %} {{ params.classes }}{% endif %}" {%- if params.id %} id="{{ params.id }}"{% endif %}>
|
||||
<div class="usa-alert__body">
|
||||
<p class="usa-alert__text">
|
||||
{{params.text}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="usa-alert usa-alert--{{params.type}} {%- if params.classes %} {{ params.classes }}{% endif %}" {%- if params.id %} id="{{ params.id }}"{% endif %}>
|
||||
<div class="usa-alert__body">
|
||||
<h{{params.header_level}} class="usa-alert__heading">{{params.heading}}</h{{params.header_level}}>
|
||||
<p class="usa-alert__text">
|
||||
{{params.text | safe }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -76,18 +76,23 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-2">
|
||||
<div class="usa-nav__secondary">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% for item in params.secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value
|
||||
in item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- <li class="usa-nav__secondary-item">
|
||||
<a href="javascript:void(0)">
|
||||
Sign in
|
||||
</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
<!-- <section aria-label="Search component">
|
||||
<form class="usa-search usa-search--small margin-bottom-2" role="search">
|
||||
|
||||
15
app/templates/components/components/textarea/README.md
Normal file
15
app/templates/components/components/textarea/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Textarea
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the textarea component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/textarea).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
|
||||
See [options table](https://design-system.service.gov.uk/components/textarea/#options-example-default) for details.
|
||||
@@ -0,0 +1,85 @@
|
||||
[
|
||||
{
|
||||
"name": "id",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The id of the textarea."
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The name of the textarea, which is submitted with the form data."
|
||||
},
|
||||
{
|
||||
"name": "rows",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Optional number of textarea rows (default is 5 rows)."
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Optional initial value of the textarea."
|
||||
},
|
||||
{
|
||||
"name": "describedBy",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users."
|
||||
},
|
||||
{
|
||||
"name": "label",
|
||||
"type": "object",
|
||||
"required": true,
|
||||
"description": "Options for the label component.",
|
||||
"isComponent": true
|
||||
},
|
||||
{
|
||||
"name": "hint",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Options for the hint component.",
|
||||
"isComponent": true
|
||||
},
|
||||
{
|
||||
"name": "errorMessage",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Options for the errorMessage component (e.g. text).",
|
||||
"isComponent": true
|
||||
},
|
||||
{
|
||||
"name": "formGroup",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Options for the form-group wrapper",
|
||||
"params": [
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the form group (e.g. to show error state for the whole group)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the textarea."
|
||||
},
|
||||
{
|
||||
"name": "autocomplete",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Attribute to [identify input purpose](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose.html), for instance \"postal-code\" or \"username\". See [autofill](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill) for full list of attributes that can be used."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the textarea."
|
||||
}
|
||||
]
|
||||
3
app/templates/components/components/textarea/macro.njk
Normal file
3
app/templates/components/components/textarea/macro.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro govukTextarea(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
44
app/templates/components/components/textarea/template.njk
Normal file
44
app/templates/components/components/textarea/template.njk
Normal file
@@ -0,0 +1,44 @@
|
||||
{% from "../error-message/macro.njk" import usaErrorMessage -%}
|
||||
{% from "../hint/macro.njk" import usaHint %}
|
||||
{% from "../label/macro.njk" import usaLabel %}
|
||||
|
||||
{#- a record of other elements that we need to associate with the input using
|
||||
aria-describedby – for example hints or error messages -#}
|
||||
{% set describedBy = params.describedBy if params.describedBy else "" %}
|
||||
<div class="usa-form-group {%- if params.errorMessage %} usa-form-group--error{% endif %} {%- if params.formGroup.classes %} {{ params.formGroup.classes }}{% endif %}">
|
||||
{{ usaLabel({
|
||||
html: params.label.html,
|
||||
text: params.label.text,
|
||||
classes: params.label.classes,
|
||||
isPageHeading: params.label.isPageHeading,
|
||||
attributes: params.label.attributes,
|
||||
for: params.id
|
||||
}) | indent(2) | trim }}
|
||||
{% if params.hint %}
|
||||
{% set hintId = params.id + '-hint' %}
|
||||
{% set describedBy = describedBy + ' ' + hintId if describedBy else hintId %}
|
||||
{{ usaHint({
|
||||
id: hintId,
|
||||
classes: params.hint.classes,
|
||||
attributes: params.hint.attributes,
|
||||
html: params.hint.html,
|
||||
text: params.hint.text
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
{% if params.errorMessage %}
|
||||
{% set errorId = params.id + '-error' %}
|
||||
{% set describedBy = describedBy + ' ' + errorId if describedBy else errorId %}
|
||||
{{ usaErrorMessage({
|
||||
id: errorId,
|
||||
classes: params.errorMessage.classes,
|
||||
attributes: params.errorMessage.attributes,
|
||||
html: params.errorMessage.html,
|
||||
text: params.errorMessage.text,
|
||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
<textarea class="usa-textarea {{- ' usa-textarea--error' if params.errorMessage }} {{- ' ' + params.classes if params.classes}}" id="{{ params.id }}" name="{{ params.name }}" rows="{%if params.rows %} {{- params.rows -}} {% else %}5{%endif %}"
|
||||
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
|
||||
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete}}"{% endif %}
|
||||
{%- for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}>{{ params.value }}</textarea>
|
||||
</div>
|
||||
6
app/templates/components/show-more.html
Normal file
6
app/templates/components/show-more.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% macro show_more(url, label, with_border=True) %}
|
||||
<a
|
||||
href="{{ url }}"
|
||||
class="usa-link show-more{% if not with_border %}-no-border{% endif %}"
|
||||
><span>{{ label }}</span></a>
|
||||
{% endmacro %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False) -%}
|
||||
<table class="usa-table{{' usa-table--borderless' if not border_visible}} width-full">
|
||||
{% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False) -%}
|
||||
<table class="usa-table usa-table--borderless width-full">
|
||||
<caption class="font-body-lg table-heading{{ ' usa-sr-only' if not caption_visible}}">
|
||||
{{ caption }}
|
||||
</caption>
|
||||
@@ -22,10 +22,10 @@
|
||||
</table>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro list_table(items, caption='', empty_message='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False) -%}
|
||||
{% macro list_table(items, caption='', empty_message='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False) -%}
|
||||
{% set parent_caller = caller %}
|
||||
|
||||
{% call mapping_table(caption, field_headings, field_headings_visible, caption_visible, equal_length, border_visible) %}
|
||||
{% call mapping_table(caption, field_headings, field_headings_visible, caption_visible, equal_length) %}
|
||||
{% for item in items %}
|
||||
{% call row(item.id) %}
|
||||
{{ parent_caller(item, loop.index + 1) }}
|
||||
@@ -69,7 +69,7 @@
|
||||
'table-field-right-aligned' if align == 'right' else
|
||||
'table-field-center-aligned' if align == 'center' else
|
||||
'table-field-left-aligned'
|
||||
%}
|
||||
%}
|
||||
{% set border = '' if border else 'table-field-noborder' %}
|
||||
{% set wrap = 'table-field-wrap-text' if wrap else '' %}
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
{% if notification.status|format_notification_status_as_url(notification.notification_type) %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<p class="status-hint margin-0 width-card ">
|
||||
<p class="status-hint margin-0 width-card-lg">
|
||||
{{ notification.status|format_notification_status_as_time(
|
||||
notification.created_at|format_datetime_short,
|
||||
(notification.updated_at or notification.created_at)|format_datetime_short
|
||||
@@ -180,42 +180,6 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro notification_carrier_field(notification) %}
|
||||
|
||||
<!-- {% set displayed_on_single_line = notification.status in ['created', 'pending', 'sending', 'delivered', 'accepted', 'received'] %} -->
|
||||
|
||||
{% if not notification %}
|
||||
{% call field(align='right') %}{% endcall %}
|
||||
{% else %}
|
||||
<!-- {% set status = notification.status|format_notification_status_as_field_status(notification.notification_type) %} -->
|
||||
{% call field(
|
||||
align='right'
|
||||
) %}
|
||||
<p class="status-hint margin-0 width-card">
|
||||
{{ notification.carrier}}
|
||||
</p>
|
||||
{% if displayed_on_single_line %}</span>{% endif %}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro notification_carrier_message_field(notification) %}
|
||||
|
||||
{% if not notification %}
|
||||
{% call field(align='right') %}{% endcall %}
|
||||
{% else %}
|
||||
<!-- {% set status = notification.status|format_notification_status_as_field_status(notification.notification_type) %} -->
|
||||
{% call field(
|
||||
status=status,
|
||||
align='right'
|
||||
) %}
|
||||
<p class="status-hint margin-0 width-card">
|
||||
{{notification.provider_response}}
|
||||
</p>
|
||||
{% if displayed_on_single_line %}</span>{% endif %}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro spark_bar_field(
|
||||
count,
|
||||
@@ -225,7 +189,7 @@
|
||||
{% call field(align='right') %}
|
||||
<span {% if id %}id="{{ id }}"{% endif %} class="spark-bar">
|
||||
<span class="spark-bar-bar">
|
||||
{{ '{:,.0f}'.format(count) }}
|
||||
{{ '{:,.0f}'.format(count) }}
|
||||
<!-- {% if count == 1 -%}
|
||||
message sent
|
||||
{% else -%}
|
||||
|
||||
@@ -19,22 +19,17 @@
|
||||
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 %}"
|
||||
>
|
||||
{% 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 }}">
|
||||
{% if label %}
|
||||
{{ label }}
|
||||
{% else %}
|
||||
{{ field.label.text }}
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<span class="error-message" 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 %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% if hint %}
|
||||
<div class="usa-hint">
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
{% macro tick_cross(yes, label, truthy_hint='Can', falsey_hint='Cannot') %}
|
||||
<li>
|
||||
{% if yes %}
|
||||
<li>
|
||||
<span class="tick-cross-tick">
|
||||
<span class="usa-sr-only">{{ truthy_hint }}</span>
|
||||
{{ label}}
|
||||
</span>
|
||||
</li>
|
||||
{% else %}
|
||||
<span class="tick-cross-cross">
|
||||
<span class="usa-sr-only">{{ falsey_hint }}</span>
|
||||
{{ label}}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro tick_cross_done_not_done(yes, label) %}
|
||||
|
||||
10
app/templates/fullwidth_template.html
Normal file
10
app/templates/fullwidth_template.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "admin_template.html" %}
|
||||
|
||||
{% set mainClasses = "" %}
|
||||
|
||||
{% block main %}
|
||||
{% block beforeContent %}{% endblock %}
|
||||
<main id="main-content" role="main">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -13,12 +13,13 @@
|
||||
{% if not current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% endif %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
{% endif %}
|
||||
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
{% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||
{% endif %} -->
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_api_keys') %}
|
||||
<!-- <li><a class="usa-link{{ main_navigation.is_selected('api-integration') }}" href="{{ url_for('.api_integration', service_id=current_service.id) }}">API integration</a></li> -->
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% from "components/big-number.html" import big_number %}
|
||||
{% from "components/pill.html" import pill %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
@@ -5,40 +6,11 @@
|
||||
<div class="grid-row ">
|
||||
{% for label, query_param, url, count in counts %}
|
||||
{% if query_param == 'pending' %}
|
||||
<div class="grid-col-3">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ query_param }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid-col-3">{{ big_number(count, query_param, smaller=True) }}</div>
|
||||
{% else %}
|
||||
<div class="grid-col-3">
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{% if count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid-col-3">{{ big_number(count, label, smaller=True) }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{% if help %}
|
||||
{% include 'partials/tour.html' %}
|
||||
{% else %}
|
||||
<nav class="nav">
|
||||
<ul class="usa-sidenav">
|
||||
{# {% if current_user.has_permissions() %} #}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}"
|
||||
href="{{ url_for('main.service_settings', service_id=current_service.id) }}">General</a></li>
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('user-profile') }}"
|
||||
href="{{ url_for('main.user_profile', service_id=current_service.id) }}">User profile</a></li>
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('team-members') }}"
|
||||
href="{{ url_for('main.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
{# {% endif %} #}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@@ -1,34 +0,0 @@
|
||||
{% extends "admin_template.html" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{% block service_page_title %}{% endblock %} – {{ current_service.name }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-row margin-top-5">
|
||||
{% if help %}
|
||||
<div class="grid-col-3">
|
||||
{% else %}
|
||||
<div class="grid-col-3">
|
||||
{% endif %}
|
||||
{% include "settings_nav.html" %}
|
||||
</div>
|
||||
{% if help %}
|
||||
<div class="grid-col-8">
|
||||
{% else %}
|
||||
<div class="grid-col-9 padding-left-4">
|
||||
{% endif %}
|
||||
{% block beforeContent %}
|
||||
{% block backLink %}{% endblock %}
|
||||
{% endblock %}
|
||||
<main id="main-content" role="main" class="usa-prose site-prose margin-bottom-10">
|
||||
{% block content %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% block maincolumn_content %}{% endblock %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
14
app/templates/support-tickets/branding-request.txt
Normal file
14
app/templates/support-tickets/branding-request.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
Organization: {% if current_service.organization -%}
|
||||
{{ current_service.organization.name }}
|
||||
{%- else -%}
|
||||
Can’t tell (domain is {{ current_user.email_domain }})
|
||||
{%- endif %}
|
||||
Service: {{ current_service.name }}
|
||||
{{ url_for('main.service_dashboard', service_id=current_service.id, _external=True) }}
|
||||
|
||||
---
|
||||
Current branding: {{ current_branding }}
|
||||
Branding requested: {{ branding_requested }}
|
||||
{% if detail %}
|
||||
{{ detail }}
|
||||
{% endif %}
|
||||
31
app/templates/support-tickets/go-live-request.txt
Normal file
31
app/templates/support-tickets/go-live-request.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
{% set service = current_service -%}
|
||||
{% set organization = service.organization -%}
|
||||
{% set user = current_user -%}
|
||||
|
||||
Service: {{ service.name }}
|
||||
{{ url_for('main.service_dashboard', service_id=service.id, _external=True) }}
|
||||
|
||||
---
|
||||
Organization type: {{ service.organization_type_label }}
|
||||
{%- if organization.name %} (organization is {{ organization.name }})
|
||||
{%- else %} (domain is {{ user.email_domain }})
|
||||
{%- endif %}.
|
||||
{%- if organization.request_to_go_live_notes %} {{ organization.request_to_go_live_notes }}{% endif %}
|
||||
{%- if organization.agreement_signed_by %}
|
||||
Agreement signed by: {{ organization.agreement_signed_by.email_address }}
|
||||
{% endif -%}
|
||||
{%- if organization.agreement_signed_on_behalf_of_email_address -%}
|
||||
Agreement signed on behalf of: {{ organization.agreement_signed_on_behalf_of_email_address }}
|
||||
{%- endif %}
|
||||
|
||||
Emails in next year: {{ service.volume_email|format_thousands }}
|
||||
Text messages in next year: {{ service.volume_sms|format_thousands }}
|
||||
|
||||
Consent to research: {{ service.consent_to_research|format_yes_no }}
|
||||
Other live services for that user: {{ user.live_services|format_yes_no }}
|
||||
|
||||
Service reply-to address: {{ service.default_email_reply_to_address or "not set" }}
|
||||
|
||||
---
|
||||
Request sent by {{ user.email_address }}
|
||||
Requester’s user page: {{ url_for('main.user_information', user_id=user.id, _external=True) }}
|
||||
5
app/templates/support-tickets/support-ticket.txt
Normal file
5
app/templates/support-tickets/support-ticket.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
{{ content }}
|
||||
{% if current_service -%}
|
||||
Service: "{{ current_service.name }}"
|
||||
{{ url_for('main.service_dashboard', service_id=current_service.id, _external=True) }}
|
||||
{% endif %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/previous-next-navigation.html" import previous_next_navigation %}
|
||||
{% 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 %}
|
||||
{% from "components/table.html" import list_table, field, text_field, link_field, right_aligned_field_heading, hidden_field_heading, row_heading, notification_status_field %}
|
||||
|
||||
<div class="ajax-block-container" id='pill-selected-item'>
|
||||
|
||||
@@ -12,19 +12,17 @@
|
||||
caption="Recent activity",
|
||||
caption_visible=False,
|
||||
empty_message='No messages found  (messages are kept for {} days)'.format(limit_days)|safe,
|
||||
field_headings=['Recipient', 'Status', 'Carrier', 'Carrier Response'],
|
||||
field_headings=['Recipient', 'Status'],
|
||||
field_headings_visible=False
|
||||
) %}
|
||||
{% call row_heading() %}
|
||||
<a class="usa-link file-list-filename" href="{{ single_notification_url(notification_id=item.id) }}">{{ item.to.splitlines()|join(', ') if item.to else '' }}</a>
|
||||
<p class="file-list-hint width-card">
|
||||
<p class="file-list-hint">
|
||||
{{ item.preview_of_content }}
|
||||
</p>
|
||||
{% endcall %}
|
||||
|
||||
{{ notification_status_field(item) }}
|
||||
{{ notification_carrier_field(item) }}
|
||||
{{ notification_carrier_message_field(item)}}
|
||||
|
||||
{% endcall %}
|
||||
{% if notifications %}
|
||||
|
||||
@@ -10,12 +10,7 @@ Error
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
<!--hide back link for one-off sends because the user never created a csv file-->
|
||||
{% if recipients|length == 1 and not recipients.allowed_to_send_to and not recipients.missing_column_headers %}
|
||||
<!--do nothing-->
|
||||
{% else %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{% endif %}
|
||||
{{ usaBackLink({ "href": back_link }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
@@ -135,10 +130,7 @@ Error
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
<!--hide the upload button and back to top link for one off sends-->
|
||||
{% if recipients|length == 1 and not recipients.allowed_to_send_to and not recipients.missing_column_headers %}
|
||||
<!-- do nothing -->
|
||||
{% else %}
|
||||
|
||||
<div class="js-stick-at-top-when-scrolling">
|
||||
<div class="form-group">
|
||||
{% if not request.args.from_test %}
|
||||
@@ -152,7 +144,6 @@ Error
|
||||
</div>
|
||||
<a href="#content" class="usa-link back-to-top-link">Back to top</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not request.args.from_test %}
|
||||
|
||||
@@ -219,4 +210,4 @@ recipients.column_headers %}
|
||||
<h2 class="font-body-lg">Preview of {{ template.name }}</h2>
|
||||
{{ template|string }}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -4,6 +4,7 @@
|
||||
{% from "components/table.html" import mapping_table, row, field, text_field, index_field, hidden_field_heading %}
|
||||
{% from "components/file-upload.html" import file_upload %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
{% from "components/components/alert/macro.njk" import usaAlert %}
|
||||
|
||||
{% block service_page_title %}
|
||||
Error
|
||||
|
||||
151
app/templates/views/cookies.html
Normal file
151
app/templates/views/cookies.html
Normal file
@@ -0,0 +1,151 @@
|
||||
{% extends "withoutnav_template.html" %}
|
||||
{% from "components/banner.html" import banner %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Cookies
|
||||
{% endblock %}
|
||||
|
||||
{% block cookie_message %}{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-8">
|
||||
<div class="cookie-settings__confirmation banner banner-with-tick" data-cookie-confirmation="true" role="group" tabindex="-1">
|
||||
<h2 class="banner-title">Your cookie settings were saved</h2>
|
||||
<a class="govuk_link cookie-settings__prev-page" href="#" data-module="track-click" data-track-category="cookieSettings" data-track-action="Back to previous page">
|
||||
Go back to the page you were looking at
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="font-body-2xl margin-bottom-3">Cookies</h1>
|
||||
<p class="summary">
|
||||
Cookies are small files saved on your phone, tablet, or computer when you visit a website.
|
||||
</p>
|
||||
<p>We use cookies to make Notify.gov work and collect information about how you use our service.</p>
|
||||
|
||||
<h2 class="font-body-lg" id="essential-cookies">Essential cookies</h2>
|
||||
<p>
|
||||
Essential cookies keep your information secure while you use Notify. We do not need to ask permission to use them.
|
||||
</p>
|
||||
<table>
|
||||
<caption class="usa-sr-only">Essential cookies</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Purpose</th>
|
||||
<th>Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
notify_admin_session
|
||||
</td>
|
||||
<td width="50%">
|
||||
Used to keep you signed in
|
||||
</td>
|
||||
<td>
|
||||
20 hours
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
cookie_policy
|
||||
</td>
|
||||
<td width="50%">
|
||||
Saves your cookie consent settings
|
||||
</td>
|
||||
<td>
|
||||
1 year
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 class="font-body-lg" id="analytics-cookies">Analytics cookies (optional)</h2>
|
||||
<p>
|
||||
With your permission, we use Google Analytics to collect data about how you use Notify. This information helps us to improve our service.
|
||||
</p>
|
||||
<p>
|
||||
Google is not allowed to use or share our analytics data with anyone.
|
||||
</p>
|
||||
<p>
|
||||
Google Analytics stores anonymized information about:
|
||||
</p>
|
||||
<ul class="usa-list usa-list--bullet">
|
||||
<li>how you got to Notify.gov</li>
|
||||
<li>the pages you visit on Notify and how long you spend on them</li>
|
||||
<li>any errors you see while using Notify</li>
|
||||
</ul>
|
||||
<table>
|
||||
<caption class="usa-sr-only">Google Analytics cookies</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Purpose</th>
|
||||
<th>Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
_ga
|
||||
</td>
|
||||
<td width="50%">
|
||||
Checks if you’ve visited Notify before. This helps us count how many people visit our site.
|
||||
</td>
|
||||
<td>
|
||||
2 years
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
_gid
|
||||
</td>
|
||||
<td width="50%">
|
||||
Checks if you’ve visited Notify before. This helps us count how many people visit our site.
|
||||
</td>
|
||||
<td>
|
||||
24 hours
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="cookie-settings__no-js">
|
||||
<h2 class="govuk-heading-s">Do you want to accept analytics cookies?</h2>
|
||||
<p>We use Javascript to set our analytics cookies. Unfortunately Javascript is not running on your browser, so you cannot change your settings. You can try:</p>
|
||||
<ul class="usa-list usa-list--bullet">
|
||||
<li>reloading the page</li>
|
||||
<li>turning on Javascript in your browser</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cookie-settings__form-wrapper">
|
||||
<form data-module="cookie-settings">
|
||||
<div class="usa-form-group">
|
||||
<fieldset class="usa-fieldset" aria-describedby="changed-name-hint">
|
||||
<legend class="usa-fieldset__legend usa-fieldset__legend--s">
|
||||
Do you want to accept analytics cookies?
|
||||
</legend>
|
||||
<div class="usa-radios usa-radios--inline">
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" id="cookies-analytics-yes" name="cookies-analytics" type="radio" value="on">
|
||||
<label class="usa-radio__label" for="cookies-analytics-yes">
|
||||
Yes
|
||||
</label>
|
||||
</div>
|
||||
<div class="usa-radio">
|
||||
<input class="usa-radio__input" id="cookies-analytics-no" name="cookies-analytics" type="radio" value="off">
|
||||
<label class="usa-radio__label" for="cookies-analytics-no">
|
||||
No
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<button class="govuk-button" type="submit">Save cookie settings</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
||||
{% from "components/big-number.html" import big_number -%}
|
||||
|
||||
<div class='dashboard-table ajax-block-container'>
|
||||
{% call(item, row_number) list_table(
|
||||
@@ -35,78 +36,28 @@
|
||||
{% endcall %}
|
||||
{% call field() %}
|
||||
{% if item.scheduled %}
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notification_count is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, item.notification_count) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(item.notification_count) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ item.notification_count }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if item.notification_count %}
|
||||
<span class="big-number-label">{{ item.notification_count|message_count_label(item.template_type,suffix='waiting to send') }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ big_number(
|
||||
item.notification_count,
|
||||
smallest=True,
|
||||
label=item.notification_count|message_count_label(
|
||||
item.template_type,
|
||||
suffix='waiting to send'
|
||||
)
|
||||
) }}
|
||||
{% else %}
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-4">
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{{ "{:,}".format(item.notifications_sending) }}
|
||||
</span>
|
||||
<span class="big-number-label">pending</span>
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ big_number(
|
||||
item.notifications_sending,
|
||||
smallest=True,
|
||||
label='pending',
|
||||
) }}
|
||||
</div>
|
||||
<div class="grid-col-4">
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notifications_delivered is number %}
|
||||
{{ "{:,}".format(item.notifications_delivered) }}
|
||||
{% else %}
|
||||
{{ item.notifications_delivered }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">delivered</span>
|
||||
</span>
|
||||
{{ big_number(item.notifications_delivered, smallest=True, label='delivered') }}
|
||||
</div>
|
||||
<div class="grid-col-4">
|
||||
{% if link %}
|
||||
<a class="usa-link display-flex" href="{{ link }}">
|
||||
{% endif %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if item.notifications_failed is number %}
|
||||
{% if currency %}
|
||||
{{ "{}{:,.2f}".format(currency, item.notifications_failed) }}
|
||||
{% else %}
|
||||
{{ "{:,}".format(item.notifications_failed) }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ item.notifications_failed }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">failed</span>
|
||||
</span>
|
||||
{% if link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{{ big_number(item.notifications_failed, smallest=True, label='failed') }}
|
||||
</div></div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
{% from "components/big-number.html" import big_number_with_status %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
<div class="grid-row grid-gap">
|
||||
<div id="total-sms" class="grid-col-12">
|
||||
<span class="big-number-with-status">
|
||||
<a class="usa-link display-flex" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='sending,delivered,failed') }}">
|
||||
<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') }}</span>
|
||||
</span>
|
||||
</a>
|
||||
{% if show_failures %}
|
||||
<span class="big-number-status{% if statistics['sms']['show_warning'] is sameas true %} big-number-status--failing{% endif %}">
|
||||
{% if statistics['sms']['failed'] %}
|
||||
<a class="usa-link" href="{{ url_for('.view_notifications', service_id=service_id, message_type='sms', status='failed') }}">
|
||||
{{ "{:,}".format(statistics['sms']['failed']) }}
|
||||
failed – {{ statistics['sms']['failed_percentage'] }}%
|
||||
</a>
|
||||
{% else %}
|
||||
No failures
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{{ big_number_with_status(
|
||||
statistics['sms']['requested'],
|
||||
statistics['sms']['requested']|message_count_label('sms', suffix='sent'),
|
||||
statistics['sms']['failed'],
|
||||
statistics['sms']['failed_percentage'],
|
||||
statistics['sms']['show_warning'],
|
||||
failure_link=url_for(".view_notifications", service_id=service_id, message_type='sms', status='failed'),
|
||||
link=url_for(".view_notifications", service_id=service_id, message_type='sms', status='sending,delivered,failed'),
|
||||
smaller=True,
|
||||
) }}
|
||||
</div>
|
||||
<!-- <div id="total-email" class="grid-col-6">
|
||||
<span class="big-number-with-status pilot-disabled">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %}
|
||||
{% from "components/show-more.html" import show_more %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
{% if current_service.scheduled_job_stats.count %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% from "components/big-number.html" import big_number %}
|
||||
|
||||
<div class='grid-row grid-gap ajax-block-container'>
|
||||
<div class='grid-col-12'>
|
||||
|
||||
@@ -13,21 +15,17 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="big-number-smaller big-number-number">{{ "{:,}".format(sms_sent) }}</span></td>
|
||||
<td>{{ big_number(40000 - sms_allowance_remaining, smaller=True) }}</td>
|
||||
<td>
|
||||
{% if sms_cost %}
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{{ "{}{:,.2f}".format('$', sms_cost) }}
|
||||
</span>
|
||||
<span class="big-number-label">'spent on text messages'</span>
|
||||
</span>
|
||||
{{ big_number(
|
||||
sms_cost,
|
||||
'spent on text messages',
|
||||
currency="$",
|
||||
smaller=True
|
||||
) }}
|
||||
{% else %}
|
||||
<span class="big-number-smaller">
|
||||
<span class="big-number-number">
|
||||
{{ "{:,}".format(sms_allowance_remaining) }}
|
||||
</span>
|
||||
</span>
|
||||
{{ big_number(sms_allowance_remaining, smaller=True) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
|
||||
{% 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 %}
|
||||
{% from "components/show-more.html" import show_more %}
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, hidden_field_heading %}
|
||||
{% from "components/ajax-block.html" import ajax_block %}
|
||||
|
||||
{% block service_page_title %}
|
||||
@@ -29,73 +30,10 @@
|
||||
|
||||
{{ ajax_block(partials, updates_url, 'template-statistics') }}
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Recent Batches</h2>
|
||||
<div class='job-table'>
|
||||
<table class="usa-table usa-table--borderless width-full">
|
||||
<thead class="table-field-headings">
|
||||
<tr>
|
||||
<th scope="col" class="table-field-heading-first file-name">
|
||||
<span>File name</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading template">
|
||||
<span>Template</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading time-sent">
|
||||
<span>Time sent</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading sender">
|
||||
<span>Sender</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading #-of-recipients">
|
||||
<span># of Recipients</span>
|
||||
</th>
|
||||
<th scope="col" class="table-field-heading report">
|
||||
<span>Report</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for notification in notifications[:5] %}
|
||||
{% if notification %}
|
||||
<tr class="table-row" id="{{ notification.job.id }}">
|
||||
<th class="table-field">
|
||||
{{ notification.job.original_file_name if notification.job.original_file_name else 'Manually entered number'}}
|
||||
<br>
|
||||
<a class="usa-link file-list-filename" href="/services/{{ notification.service }}/jobs/{{ notification.job.id }}">View Batch</a>
|
||||
</th>
|
||||
<th class="table-field">
|
||||
{{ notification.template.name }}
|
||||
</th>
|
||||
<th class="table-field">
|
||||
{{ notification.created_at | format_datetime_short_america }}
|
||||
</th>
|
||||
<th class="table-field">
|
||||
{{ notification.created_by.name }}
|
||||
</th>
|
||||
{% set job_available = jobs|selectattr('job_id', 'equalto', notification.job.id)|first %}
|
||||
<th class="table-field">
|
||||
{{ job_available.notification_count if job_available else ''}}
|
||||
</th>
|
||||
<th class="table-field">
|
||||
{% if job_available and job_available.time_left != "Data no longer available" %}
|
||||
<a class="usa-link file-list-filename" href="{{ job_available.download_link }}">
|
||||
{{ "Download" if job_available.job_id else '' }}</a>
|
||||
<span class="usa-hint">{{ job_available.time_left }}</span>
|
||||
{% elif job_available %}
|
||||
<span>{{ job_available.time_left }}</span>
|
||||
{% endif %}
|
||||
</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 class="margin-top-4 margin-bottom-1">Usage</h2>
|
||||
<h3 class="margin-bottom-0">Daily</h3>
|
||||
<p class="margin-0">Across all services</p>
|
||||
<table class="usage-table usa-table usa-table--borderless margin-top-1 margin-bottom-5">
|
||||
<table class="usa-table usa-table--borderless margin-top-1 margin-bottom-5">
|
||||
<caption class="usa-sr-only">
|
||||
Daily
|
||||
</caption>
|
||||
@@ -107,7 +45,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ global_message_limit - daily_global_messages_remaining }}</td>
|
||||
<td>{{ 5000 - daily_global_messages_remaining }}</td>
|
||||
<td>
|
||||
{{ daily_global_messages_remaining }}
|
||||
</td>
|
||||
@@ -116,12 +54,12 @@
|
||||
</table>
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
<h3 class='margin-bottom-0' id="current-year"></h3>
|
||||
<h3 class='margin-bottom-0'>2023</h3>
|
||||
{{ ajax_block(partials, updates_url, 'usage') }}
|
||||
<a
|
||||
href="{{ url_for('.usage', service_id=current_service['id']) }}"
|
||||
class="usa-link show-more"
|
||||
><span>See all usage</span></a>
|
||||
{{ show_more(
|
||||
url_for(".usage", service_id=current_service['id']),
|
||||
'See all usage'
|
||||
) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/big-number.html" import big_number_with_status, big_number %}
|
||||
{% from "components/pill.html" import pill %}
|
||||
{% from "components/table.html" import list_table, field, hidden_field_heading, right_aligned_field_heading, row_heading %}
|
||||
|
||||
@@ -43,16 +44,11 @@
|
||||
(month.email_counts, 'email'),
|
||||
] %}
|
||||
{% call field(align='left') %}
|
||||
<span class="big-number-smallest">
|
||||
<span class="big-number-number">
|
||||
{% if counts.requested is number %}
|
||||
{{ "{:,}".format(counts.requested) }}
|
||||
{% else %}
|
||||
{{ counts.requested }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="big-number-label">{{ counts.requested|message_count_label(template_type, suffix='') }}</span>
|
||||
</span>
|
||||
{{ big_number(
|
||||
counts.requested,
|
||||
counts.requested|message_count_label(template_type, suffix=''),
|
||||
smallest=True,
|
||||
) }}
|
||||
{% if counts.requested %}
|
||||
<span class="{{ 'failure-highlight' if counts.show_warning else '' }}">
|
||||
{{ "{:,}".format(counts.failed) }} failed
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{% from "components/big-number.html" import big_number %}
|
||||
{% from "components/big-number.html" import big_number %}
|
||||
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, spark_bar_field %}
|
||||
{% from "components/show-more.html" import show_more %}
|
||||
|
||||
<div class="ajax-block-container">
|
||||
{% if template_statistics|length > 1 %}
|
||||
@@ -24,10 +27,11 @@
|
||||
|
||||
{{ spark_bar_field(item.count, most_used_template_count, id=item.template_id) }}
|
||||
{% endcall %}
|
||||
<a
|
||||
href="{{ url_for('.template_usage', service_id=current_service.id) }}"
|
||||
class="usa-link show-more-no-border"
|
||||
><span>See templates used by month</span></a>
|
||||
{{ show_more(
|
||||
url_for('.template_usage', service_id=current_service.id),
|
||||
'See templates used by month',
|
||||
with_border=False
|
||||
) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
{% from "components/components/alert/macro.njk" import usaAlert %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ heading_action }} text message template
|
||||
@@ -49,7 +50,7 @@
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-12">
|
||||
<div class="template-content-count" bg-color="red">
|
||||
<div class="template-content-count">
|
||||
<div data-module="update-status" data-target="template_content"
|
||||
data-updates-url="{{ url_for('.count_content_length', service_id=current_service.id, template_type='sms') }}"
|
||||
aria-live="polite">
|
||||
@@ -68,7 +69,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
|
||||
<h2 id="help" class="font-body-xl margin-top-2">How to customize your message</h2>
|
||||
<div class="usa-accordion usa-accordion--bordered usa-accordion--multiselectable maxw-mobile-lg" data-allow-multiple>
|
||||
|
||||
46
app/templates/views/email-branding/manage-branding.html
Normal file
46
app/templates/views/email-branding/manage-branding.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "views/platform-admin/_base_template.html" %}
|
||||
{% from "components/file-upload.html" import file_upload %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% block service_page_title %}
|
||||
{{ '{} email branding'.format('Update' if email_branding else 'Create')}}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": url_for('.email_branding') }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block platform_admin_content %}
|
||||
|
||||
{{ page_header('{} email branding'.format('Update' if email_branding else 'Add')) }}
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-9">
|
||||
{% if logo %}
|
||||
<div id="logo-img">
|
||||
<img src="https://{{ cdn_url }}/{{ logo }}"/>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>
|
||||
Logos should be PNG files, 108px high
|
||||
</p>
|
||||
{{ file_upload(form.file, allowed_file_extensions=['png'], button_text='{} logo'.format('Update' if email_branding else 'Upload')) }}
|
||||
{% call form_wrapper() %}
|
||||
<div class="form-group">
|
||||
<div style='margin-top:15px;'>{{form.name}}</div>
|
||||
<div style='margin-top:15px;'>{{form.text}}</div>
|
||||
{{ form.colour }}
|
||||
{{ form.brand_type }}
|
||||
{{ page_footer(
|
||||
'Save',
|
||||
button_name='operation',
|
||||
button_value='email-branding-details'
|
||||
) }}
|
||||
</div>
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
36
app/templates/views/email-branding/select-branding.html
Normal file
36
app/templates/views/email-branding/select-branding.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "views/platform-admin/_base_template.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/live-search.html" import live_search %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
|
||||
{% set page_title = "Email branding" %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block platform_admin_content %}
|
||||
|
||||
<h1 class="font-body-2xl">{{ page_title }}</h1>
|
||||
{{ live_search(target_selector='.message-name', show=True, form=search_form, autofocus=True) }}
|
||||
<nav>
|
||||
<ul class="email-brand">
|
||||
{% for brand in email_brandings %}
|
||||
<li class="message-name">
|
||||
<a class="usa-link" href="{{ url_for('.update_email_branding', branding_id=brand.id) }}">
|
||||
{{ brand.name or 'Unnamed' }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
{{ usaButton({
|
||||
"element": "a",
|
||||
"text": "New brand",
|
||||
"href": url_for('.create_email_branding'),
|
||||
"classes": "govuk-button--secondary"
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -13,6 +13,7 @@
|
||||
<p class="usa-body">If you work for the government, you can use Notify.gov to keep your users updated.</p>
|
||||
<p class="usa-body">Notify makes it easy to create, customize, and send <a class="usa-link" href="{{ url_for('main.features_sms') }}">text messages</a>.</p>
|
||||
<!-- <ul class="list list-bullet">
|
||||
<li><a class="usa-link" href="{{ url_for('main.features_email') }}">emails</a></li>
|
||||
<li><a class="usa-link" href="{{ url_for('main.features_sms') }}">text messages</a></li>
|
||||
</ul> -->
|
||||
<p class="usa-body">You do not need any technical knowledge to use Notify.</p>
|
||||
|
||||
49
app/templates/views/features/emails.html
Normal file
49
app/templates/views/features/emails.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "content_template.html" %}
|
||||
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Emails
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
|
||||
<h1 class="font-body-2xl">Emails</h1>
|
||||
<p>Send an unlimited number of emails for free with Notify.gov.</p>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<p><a class="usa-link" href="{{ url_for('main.register') }}">Create an account</a> and try Notify for yourself.</p>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="heading heading-medium">Features</h2>
|
||||
<p>Notify makes it easy to:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>create reusable email templates</li>
|
||||
<li>personalize the content of your emails</li>
|
||||
<li>send and schedule bulk messages</li>
|
||||
</ul>
|
||||
<p>You can also <a class="usa-link" href="{{ url_for('.documentation') }}">integrate with our API</a> to send emails automatically.</p>
|
||||
|
||||
<h3 class="font-body-md margin-bottom-2" id="branding">Email branding</h3>
|
||||
<p>Add your organization’s logo and brand color to email templates.</p>
|
||||
<p>See <a class="usa-link" href="{{ url_for('.branding_and_customisation', _anchor='email-branding') }}">how to change your email branding</a>.</p>
|
||||
|
||||
<h3 class="font-body-md margin-bottom-2" id="send-files">Send files by email</h3>
|
||||
<p>Notify offers a safe and reliable way to send files by email.</p>
|
||||
<p>Upload a file using our API, then send your users an email with a link to download it.</p>
|
||||
<p>Notify uses encrypted links instead of email attachments because:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>they’re more secure</li>
|
||||
<!--<li>you can track when the recipient downloads your document</li>-->
|
||||
<li>email attachments are often marked as spam</li>
|
||||
</ul>
|
||||
<p>Read our <a class="usa-link" href="{{ url_for('.documentation') }}">API documentation</a> for more information.</p>
|
||||
|
||||
<h3 class="font-body-md margin-bottom-2" id="reply-to">Add a reply-to address</h3>
|
||||
<p>Notify lets you choose the email address that users reply to.</p>
|
||||
<p>Emails with a reply-to address seem more trustworthy and are less likely to be labelled as spam.</p>
|
||||
<p>See <a class="usa-link" href="{{ url_for('.branding_and_customisation', _anchor='reply-to-address') }}">how to add a reply-to address</a>.</p>
|
||||
|
||||
<h2 class="heading heading-medium">Pricing</h2>
|
||||
<p>It’s free to send emails through Notify.</p>
|
||||
<p><a class="usa-link" href="{{ url_for('.pricing') }}">See pricing</a> for more details.</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -9,6 +9,55 @@
|
||||
|
||||
{% block content_column_content %}
|
||||
|
||||
{{ content | safe }}
|
||||
<h1 class="font-body-2xl margin-bottom-3">Get started</h1>
|
||||
|
||||
<ol class="usa-process-list">
|
||||
<li class="usa-process-list__item padding-bottom-4 margin-top-2">
|
||||
<h2 class="usa-process-list__heading line-height-sans-3">Check if Notify.gov is right for you</h2>
|
||||
<p>Read about our <a class="usa-link" href="{{ url_for('main.features') }}">features</a>, <a class="usa-link" href="{{ url_for('.pricing') }}">pricing</a> and <a class="usa-link" href="{{ url_for('main.roadmap') }}">roadmap</a>.</p>
|
||||
</li>
|
||||
|
||||
<li class="usa-process-list__item padding-bottom-4">
|
||||
<h2 class="usa-process-list__heading line-height-sans-3">Create an account</h2>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<p><a class="usa-link" href="{{ url_for('.register') }}">Create an account</a> for free and add your first Notify service. When you add a new service it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% else %}
|
||||
<p>Create an account for free and add your first Notify service. When you add a new service, it will start in <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">trial mode</a>.</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<li class="usa-process-list__item padding-bottom-4">
|
||||
<h2 class="usa-process-list__heading line-height-sans-3">Write some messages</h2>
|
||||
<p>Add message templates with examples of the content you plan to send. You can use our <a class="usa-link" href="{{ url_for('main.guidance_index') }}">guidance</a> to help you.</p>
|
||||
</li>
|
||||
|
||||
<li class="usa-process-list__item padding-bottom-4">
|
||||
<h2 class="usa-process-list__heading line-height-sans-3">Set up your service</h2>
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>Review your settings to add message branding and sender information.</p>
|
||||
<p>Add team members and check their permissions.</p>
|
||||
{% else %}
|
||||
<p>Review your <a class="usa-link" href="{{ url_for('.service_settings', service_id=current_service.id) }}">settings</a> to add message branding and sender information.</p>
|
||||
<p>Add <a class="usa-link" href="{{ url_for('.manage_users', service_id=current_service.id) }}">team members</a> and check their permissions.</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<!-- <li class="get-started-list__item">
|
||||
<h2 class="usa-process-list__heading line-height-sans-1">Set up an API integration (optional)</h2>
|
||||
<p>You can use the Notify API to send messages automatically.</p>
|
||||
<p>Our <a class="usa-link" href="{{ url_for('main.documentation') }}">documentation</a> explains how to integrate the API with a web application or back office system.</p>
|
||||
</li> -->
|
||||
|
||||
<li class="usa-process-list__item padding-bottom-4">
|
||||
<h2 class="usa-process-list__heading line-height-sans-3">Start sending messages</h2>
|
||||
{% if not current_user.is_authenticated or not current_service %}
|
||||
<p>When you’re ready to send messages to people outside your team, go to the <b class="bold">Settings</b> page and select <b class="bold">Request to go live</b>. We’ll approve your request within one working day.</p>
|
||||
{% else %}
|
||||
<p>You should <a class="usa-link" href="{{ url_for('.request_to_go_live', service_id=current_service.id) }}">request to go live</a> when you’re ready to send messages to people outside your team. We’ll approve your request within one working day.</p>
|
||||
{% endif %}
|
||||
<!-- <p>Check <a class="usa-link" href="{{ url_for('main.how_to_pay') }}">how to pay</a> if you’re planning to exceed the <a class="usa-link" href="{{ url_for('.pricing', _anchor='text-messages') }}">free text message allowance</a>.</p> -->
|
||||
</li>
|
||||
|
||||
</ol>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
55
app/templates/views/guidance/branding-and-customisation.html
Normal file
55
app/templates/views/guidance/branding-and-customisation.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "content_template.html" %}
|
||||
{% from "components/service-link.html" import service_link %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Branding and customization
|
||||
{% endblock %}
|
||||
|
||||
{% block content_column_content %}
|
||||
|
||||
<h1 class="font-body-2xl margin-bottom-3">Branding and customization</h1>
|
||||
|
||||
<!-- <p>This page explains how to:</p>
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li><a class="usa-link" href="#email-branding">change your email branding</a></li>
|
||||
<li><a class="usa-link" href="#reply-to-address">add a reply-to email address</a></li>
|
||||
<li><a class="usa-link" href="#text-message-sender">change the text message sender</a></li>
|
||||
</ul>
|
||||
|
||||
<h2 class="font-body-lg" id="email-branding">Change your email branding</h2>
|
||||
|
||||
<p>The default branding for email templates is the Notify.govlogo.</p>
|
||||
|
||||
<p>To change your email branding:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the <b class="bold">Email settings</b> section of the {{ service_link(current_service, 'main.service_settings', 'settings') }} page.</li>
|
||||
<li>Select <b class="bold">Change</b> on the <b class="bold">Email branding</b> row.</li>
|
||||
</ol>
|
||||
|
||||
<h2 class="font-body-lg" id="reply-to-address">Add a reply-to email address</h2>
|
||||
|
||||
<p>You can choose the email address that your users reply to. You must add at least one reply-to address for your service before you can <a class="usa-link" href="{{ url_for('main.trial_mode_new') }}">request to go live</a>.</p>
|
||||
|
||||
<p>To add a reply-to email address:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the <b class="bold">Email settings</b> section of the {{ service_link(current_service, 'main.service_settings', 'settings') }} page.</li>
|
||||
<li>Select <b class="bold">Manage</b> on the <b class="bold">Reply-to email addresses</b> row.</li>
|
||||
<li>Select <b class="bold">Add reply-to address</b>.</li>
|
||||
</ol> -->
|
||||
|
||||
<h2 class="font-body-lg" id="text-message-sender">Change the text message sender</h2>
|
||||
|
||||
<p>The text message sender tells your users who the message is from.</p>
|
||||
|
||||
<p>To change the text message sender from the default of ‘Notify.gov’:</p>
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the <b class="bold">Text message settings</b> section of the {{ service_link(current_service, 'main.service_settings', 'settings') }} page.</li>
|
||||
<li>Select <b class="bold">Manage</b> on the <b class="bold">Text message senders</b> row.</li>
|
||||
<li>Select <b class="bold">Change</b> or <b class="bold">Add text message sender</b>.</li>
|
||||
</ol>
|
||||
|
||||
{% endblock %}
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li><a class="usa-link" href="#editandformat">edit and format messages</a></li>
|
||||
<li><a class="usa-link" href="#customization">add customization</a></li>
|
||||
<li><a class="usa-link" href="#branding">add branding and customization</a></li>
|
||||
<!-- <li><a class="usa-link" href="{{ url_for('.send_files_by_email') }}">send files by email</a></li> -->
|
||||
</ul>
|
||||
|
||||
<h2 class="font-body-lg" id="#formatting">Edit and format messages</h2>
|
||||
|
||||
|
||||
<p>This section explains how to:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li><a class="usa-link" href="#formatting">format your content</a></li>
|
||||
@@ -30,42 +30,42 @@
|
||||
<li><a class="usa-link" href="#personalised-content">personalize your content</a></li>
|
||||
<li><a class="usa-link" href="#optional-content">add optional content</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h3 class="font-body-lg" id="formatting">Format your content</h3>
|
||||
|
||||
|
||||
<p>You can see a list of formatting instructions on the edit template page:</p>
|
||||
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<h3 class="font-body-lg" id="links">Add links</h3>
|
||||
|
||||
|
||||
<p>When composing a text message, write URLs in full and Notify will convert them into links for you.
|
||||
</p>
|
||||
|
||||
|
||||
<p>You cannot convert text into a link.
|
||||
<!-- Research shows that people like to check a URL looks genuine before clicking the link in an email. This is hard to do if the URL is hidden behind clickable link text.-->
|
||||
</p>
|
||||
|
||||
|
||||
<p>We do not recommend using a third-party link shortening service because:</p>
|
||||
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>your users cannot see where the link will take them</li>
|
||||
<li>your link might stop working if there’s a service outage</li>
|
||||
<li>you can no longer control where the redirect goes</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<h3 class="font-body-lg" id="personalised-content">Personalize your content</h3>
|
||||
|
||||
|
||||
<p>To personalize the content of your messages, add a placeholder to the template.</p>
|
||||
|
||||
|
||||
<p>Placeholders are filled in with details, like a name or reference number, each time you send a
|
||||
message.</p>
|
||||
|
||||
|
||||
<p>To add a placeholder to the template:</p>
|
||||
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
@@ -73,20 +73,20 @@
|
||||
((ref number)).</li>
|
||||
<li>Select <b class="bold">Save</b>.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<p>When you send a message you can either:</p>
|
||||
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>manually fill in the placeholders yourself</li>
|
||||
<li>upload a list of personal details and let Notify do it for you</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>If you upload a list, the column names need to match the placeholders in your template.</p>
|
||||
|
||||
|
||||
<h3 class="font-body-lg" id="optional-content">Add optional content</h3>
|
||||
|
||||
|
||||
<p>To add optional content to your messages:</p>
|
||||
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.</li>
|
||||
<li>Add a new template or choose an existing template and select <b class="bold">Edit</b>.</li>
|
||||
@@ -94,25 +94,25 @@
|
||||
who are under 18: ((under18??Please get your application signed by a parent or guardian.))</li>
|
||||
<li>Select <b class="bold">Save</b>.</li>
|
||||
</ol>
|
||||
|
||||
|
||||
<p>For each person you send this message to, specify ‘yes’ or ‘no’ to show or hide this content. You
|
||||
can either:</p>
|
||||
|
||||
|
||||
<ul class="list list-bullet">
|
||||
<li>do this yourself</li>
|
||||
<li>upload a list of personal details and let Notify do it for you</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>If you upload a list, the column names need to match the optional content in your template.</p>
|
||||
|
||||
<h2 class="font-body-lg" id="customization">Message customization</h2>
|
||||
|
||||
<h2 class="font-body-lg" id="branding">Branding and customization</h2>
|
||||
|
||||
<h3 class="font-body-lg">Change the text message sender</h3>
|
||||
|
||||
|
||||
<p>The text message sender tells your users who the message is from.</p>
|
||||
|
||||
|
||||
<p>To change the text message sender from the default of ‘Notify.gov’:</p>
|
||||
|
||||
|
||||
<ol class="list list-number">
|
||||
<li>Go to the <b class="bold">Text message settings</b> section of the {{
|
||||
service_link(current_service, 'main.service_settings', 'settings') }} page.</li>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user