Compare commits

..

14 Commits

Author SHA1 Message Date
Andrew Shumway
eeee69cf43 Add contents keyword 2023-05-02 13:44:21 -06:00
Andrew Shumway
d3905c22ce Added read back to on push permissions 2023-05-02 13:35:20 -06:00
Andrew Shumway
dc4df683f9 Test coverage threshold in actions 2023-05-02 13:32:29 -06:00
Andrew Shumway
5c56404aac Move permissions/change output and annotations 2023-05-02 12:48:47 -06:00
Andrew Shumway
125dd56921 Uncommenting failing assert to test actions CI 2023-05-02 10:03:17 -06:00
Andrew Shumway
2c4e119a6b Merge branch 'main' of https://github.com/GSA/notifications-admin into notify-473 2023-05-02 09:47:39 -06:00
Andrew Shumway
15f3f261fb Add action to find PR number 2023-05-02 09:16:40 -06:00
Andrew Shumway
a3b18070ae Add with/test script flag 2023-05-01 14:52:12 -06:00
Andrew Shumway
3ab979347c Tests configurations 2023-05-01 14:33:51 -06:00
Andrew Shumway
6015a05d9f Tests on pull_request_target 2023-05-01 14:22:30 -06:00
Andrew Shumway
115953dbcf Fixed syntax on permissions 2023-05-01 13:49:51 -06:00
Andrew Shumway
63b16e0ae7 Test permissions on pull requests for actions 2023-05-01 13:45:45 -06:00
Andrew Shumway
647fe4f40b Tests config github actions 2023-05-01 09:33:35 -06:00
Andrew Shumway
a91ba71085 Commented out test failure/add jest config 2023-05-01 09:32:56 -06:00
800 changed files with 69419 additions and 46705 deletions

View File

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

View File

@@ -1 +0,0 @@
blank_issues_enabled: true

View File

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

View File

@@ -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 persons profile
- label: Clicking on “Remove this team member” prompts a confirmation
- label: Clicking the confirmation removes the team member
- type: checkboxes
id: sign_out
attributes:
label: Sign Out
options:
- label: Hitting “Sign Out” takes you back to the homepage and logs you out

View File

@@ -13,9 +13,9 @@ runs:
uses: actions/setup-python@v3
with:
python-version: "3.9"
- name: Install poetry
- name: Install pipenv
shell: bash
run: pip install poetry
run: pip install --upgrade pipenv
- name: Install application dependencies
shell: bash
run: make bootstrap

View File

@@ -1,16 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 3
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'

View File

@@ -33,40 +33,17 @@ jobs:
annotations: failed-tests
prnumber: ${{ steps.findPr.outputs.number }}
- name: Run style checks
run: poetry run flake8 .
run: pipenv run flake8 .
- name: Check imports alphabetized
run: poetry run isort --check-only ./app ./tests
- name: Check dead code
run: make dead-code
run: pipenv run isort --check-only ./app ./tests
- name: Run js lint
run: npm run lint
- name: Run js tests
run: npm test
- name: Run py tests with coverage
run: poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
# - name: Run E2E tests
# run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
# env:
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
# NOTIFY_E2E_TEST_URI: ${{ secrets.NOTIFY_E2E_TEST_URI }}
run: pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10
- name: Check coverage threshold
run: poetry run coverage report --fail-under=90
# - name: Health check
# run: |
# response=$(curl -url ${{secrets.NOTIFY_E2E_TEST_URI}}_status)
# if grep -q "ok" <<< "$response"; then
# echo "Health check passed"
# else
# echo "Health check failed"
# exit 1
# fi
# env:
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
# NOTIFY_E2E_TEST_URI: ${{ secrets.NOTIFY_E2E_TEST_URI }}
run: pipenv run coverage report --fail-under=90
validate-new-relic-config:
runs-on: ubuntu-latest
@@ -79,7 +56,7 @@ jobs:
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
# Need to set a NEW_RELIC_ENVIRONMENT with monitor_mode: true
NEW_RELIC_ENVIRONMENT: staging
run: poetry run newrelic-admin validate-config $NEW_RELIC_CONFIG_FILE
run: pipenv run newrelic-admin validate-config $NEW_RELIC_CONFIG_FILE
dependency-audits:
runs-on: ubuntu-latest
@@ -87,8 +64,8 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.0.6
run: pipenv requirements > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.0.4
with:
inputs: requirements.txt
- name: Run npm audit
@@ -100,7 +77,7 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-project
- name: Run scan
run: poetry run bandit -r app/ --confidence-level medium
run: pipenv run bandit -r app/ --confidence-level medium
dynamic-scan:
runs-on: ubuntu-latest
@@ -112,14 +89,14 @@ jobs:
env:
NOTIFY_ENVIRONMENT: scanning
- name: Run OWASP Baseline Scan
uses: zaproxy/action-baseline@v0.9.0
uses: zaproxy/action-baseline@v0.7.0
with:
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
target: 'http://localhost:6012'
docker_name: "owasp/zap2docker-stable"
target: "http://localhost:6012"
fail_action: true
allow_issue_writing: false
rules_file_name: 'zap.conf'
cmd_options: '-I'
rules_file_name: "zap.conf"
cmd_options: "-I"
a11y-scan:
runs-on: ubuntu-20.04

View File

@@ -25,8 +25,8 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.0.6
run: pipenv requirements > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.0.4
with:
inputs: requirements.txt
- name: Run npm audit
@@ -38,7 +38,7 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-project
- name: Run scan
run: poetry run bandit -r app/ --confidence-level medium
run: pipenv run bandit -r app/ --confidence-level medium
dynamic-scan:
runs-on: ubuntu-latest
@@ -50,9 +50,9 @@ jobs:
env:
NOTIFY_ENVIRONMENT: scanning
- name: Run OWASP Full Scan
uses: zaproxy/action-full-scan@v0.7.0
uses: zaproxy/action-full-scan@v0.4.0
with:
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
docker_name: 'owasp/zap2docker-stable'
target: 'http://localhost:6012'
fail_action: true
allow_issue_writing: false

View File

@@ -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
@@ -43,8 +43,8 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Create requirements.txt because Cloud Foundry does a weird pipenv thing
run: pipenv requirements > requirements.txt
- name: Deploy to cloud.gov
uses: 18f/cg-deploy-action@main
@@ -52,27 +52,28 @@ jobs:
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
BASIC_AUTH_PASSWORD: ${{ secrets.BASIC_AUTH_PASSWORD }}
REDIS_ENABLED: ${{ secrets.REDIS_ENABLED }}
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 }}
cf_org: gsa-tts-benefits-studio
cf_org: gsa-tts-benefits-studio-prototyping
cf_space: notify-demo
push_arguments: >-
--vars-file deploy-config/demo.yml
--var DANGEROUS_SALT="$DANGEROUS_SALT"
--var SECRET_KEY="$SECRET_KEY"
--var REDIS_ENABLED="$REDIS_ENABLED"
--var ADMIN_CLIENT_USERNAME="notify-admin"
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
--var BASIC_AUTH_USERNAME="curiousabout"
--var BASIC_AUTH_PASSWORD="$BASIC_AUTH_PASSWORD"
--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

View File

@@ -1,87 +0,0 @@
name: Deploy to production environment
on:
push:
branches: [ production ]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v41.0.0
with:
files: |
terraform/production
terraform/shared
.github/workflows/deploy-prod.yml
- name: Terraform init
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
run: terraform init
- name: Terraform apply
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
run: terraform apply -auto-approve -input=false
- uses: ./.github/actions/setup-project
- 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:
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
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 }}
cf_org: gsa-tts-benefits-studio
cf_space: notify-production
push_arguments: >-
--vars-file deploy-config/production.yml
--var DANGEROUS_SALT="$DANGEROUS_SALT"
--var SECRET_KEY="$SECRET_KEY"
--var ADMIN_CLIENT_USERNAME="notify-admin"
--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
with:
files: |
deploy-config/egress_proxy/notify-admin-production.*.acl
.github/actions/deploy-proxy/action.yml
.github/workflows/deploy-prod.yml
- name: Deploy egress proxy
if: steps.changed-egress-config.outputs.any_changed == 'true'
uses: ./.github/actions/deploy-proxy
with:
cf_space: notify-production
app: notify-admin-production

View File

@@ -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
@@ -48,9 +48,8 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Create requirements.txt because Cloud Foundry does a weird pipenv thing
run: pipenv requirements > requirements.txt
- name: Deploy to cloud.gov
uses: 18f/cg-deploy-action@main
@@ -58,42 +57,28 @@ jobs:
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
BASIC_AUTH_PASSWORD: ${{ secrets.BASIC_AUTH_PASSWORD }}
REDIS_ENABLED: ${{ secrets.REDIS_ENABLED }}
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 }}
cf_org: gsa-tts-benefits-studio
cf_org: gsa-tts-benefits-studio-prototyping
cf_space: notify-staging
push_arguments: >-
--vars-file deploy-config/staging.yml
--var DANGEROUS_SALT="$DANGEROUS_SALT"
--var SECRET_KEY="$SECRET_KEY"
--var REDIS_ENABLED="$REDIS_ENABLED"
--var ADMIN_CLIENT_USERNAME="notify-admin"
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
--var BASIC_AUTH_USERNAME="curiousabout"
--var BASIC_AUTH_PASSWORD="$BASIC_AUTH_PASSWORD"
--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

View File

@@ -45,22 +45,22 @@ jobs:
with:
path: terraform/demo
check_prod_drift:
runs-on: ubuntu-latest
name: Check for drift of production terraform configuration
environment: production
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: 'production'
# check_prod_drift:
# runs-on: ubuntu-latest
# name: Check for drift of production terraform configuration
# environment: production
# steps:
# - name: Checkout
# uses: actions/checkout@v3
# with:
# ref: 'production'
- name: Check for drift
uses: dflook/terraform-check@v1
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
with:
path: terraform/production
# - name: Check for drift
# uses: dflook/terraform-check@v1
# env:
# AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
# AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
# TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
# TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
# with:
# path: terraform/production

View File

@@ -2,7 +2,7 @@ name: Run Terraform plan in production
on:
pull_request:
branches: [ production ]
branches: [ production-disabled-for-now ]
paths: [ 'terraform/**' ]
defaults:

9
.gitignore vendored
View File

@@ -123,12 +123,3 @@ app/templates/vendor
secrets.auto.tfvars
terraform.tfstate
terraform.tfstate.backup
# Playwright
playwright/
# Pyenv
.python-version
# Nodenv
.node-version

View File

@@ -1,12 +1,12 @@
{
"defaults": {
"standard": "WCAG2AA",
"runners": ["htmlcs"],
"runners": ["axe"],
"concurrency": 1,
"hideElements": [
"nav > ol a",
"#sms-pricing-details",
"a.usa-footer__link.usa-footer__copyright-logo"
"a.govuk-footer__link.govuk-footer__copyright-logo"
]
},
"urls": [

View File

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

View File

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

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.9

View File

@@ -15,11 +15,7 @@ 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 run playwright install --with-deps
poetry run pre-commit install
pipenv install --dev
source $(NVMSH) --no-use && nvm install && npm ci --no-audit
source $(NVMSH) && npm run build
@@ -29,10 +25,10 @@ watch-frontend: ## Build frontend and watch for changes
.PHONY: run-flask
run-flask: ## Run flask
poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0
pipenv run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0
.PHONY: run-flask-bare
run-flask-bare: ## Run flask without invoking poetry so we can override ENV variables in .env
run-flask-bare: ## Run flask without invoking pipenv so we can override ENV variables in .env
flask run -p 6012 --host=0.0.0.0
.PHONY: npm-audit
@@ -51,37 +47,16 @@ generate-version-file: ## Generates the app version file
test: py-lint py-test js-lint js-test ## Run tests
.PHONY: py-lint
py-lint: ## Run python linting scanners and black
poetry self add poetry-dotenv-plugin
poetry run black .
poetry run flake8 .
poetry run isort --check-only ./app ./tests
.PHONY: avg-complexity
avg-complexity:
echo "*** Shows average complexity in radon of all code ***"
poetry run radon cc ./app -a -na
.PHONY: too-complex
too-complex:
echo "*** Shows code that got a rating of C, D or F in radon ***"
poetry run radon cc ./app -a -nc
py-lint: ## Run python linting scanners
pipenv run flake8 .
pipenv run isort --check-only ./app ./tests
.PHONY: py-test
py-test: export NEW_RELIC_ENVIRONMENT=test
py-test: ## Run python unit tests
poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
poetry run coverage report --fail-under=96
poetry run coverage html -d .coverage_cache
.PHONY: dead-code
dead-code:
poetry run vulture ./app --min-confidence=100
.PHONY: e2e-test
e2e-test: export NEW_RELIC_ENVIRONMENT=test
e2e-test: ## Run end-to-end integration tests
poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 tests/
pipenv run coverage report --fail-under=90
pipenv run coverage html -d .coverage_cache
.PHONY: js-lint
js-lint: ## Run javascript linting scanners
@@ -93,37 +68,25 @@ js-test: ## Run javascript unit tests
.PHONY: fix-imports
fix-imports: ## Fix imports using isort
poetry run isort ./app ./tests
.PHONY: py-lock
py-lock: ## Syncs dependencies and updates lock file without performing recursive internal updates
poetry lock --no-update
poetry install --sync
.PHONY: update-utils
update-utils: ## Forces Poetry to pull the latest changes from the notifications-utils repo; requires that you commit the changes to poetry.lock!
poetry update notifications-utils
@echo
@echo !!! PLEASE MAKE SURE TO COMMIT AND PUSH THE UPDATED poetry.lock FILE !!!
@echo
pipenv run isort ./app ./tests
.PHONY: freeze-requirements
freeze-requirements: ## create static requirements.txt
poetry export --without-hashes --format=requirements.txt > requirements.txt
pipenv requirements > requirements.txt
.PHONY: pip-audit
pip-audit:
poetry requirements > requirements.txt
poetry requirements --dev > requirements_for_test.txt
poetry run pip-audit -r requirements.txt
poetry run pip-audit -r requirements_for_test.txt
pipenv requirements > requirements.txt
pipenv requirements --dev > requirements_for_test.txt
pipenv run pip-audit -r requirements.txt
-pipenv run pip-audit -r requirements_for_test.txt
.PHONY: audit
audit: npm-audit pip-audit
.PHONY: static-scan
static-scan:
poetry run bandit -r app/
pipenv run bandit -r app/
.PHONY: a11y-scan
a11y-scan:

59
Pipfile Normal file
View File

@@ -0,0 +1,59 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
ago = "~=0.0.95"
blinker = "~=1.4"
fido2 = "==0.9.3"
flask = "~=2.3"
flask-basicauth = "~=0.2"
flask-login = "~=0.6"
flask-wtf = "~=1.1"
gds-metrics = {version = "==0.2.4", ref = "6f1840a57b6fb1ee40b7e84f2f18ec229de8aa72", git = "https://github.com/alphagov/gds_metrics_python.git"}
govuk-bank-holidays = "==0.11"
govuk-frontend-jinja = {version = "==0.5.8-alpha", git = "https://github.com/alphagov/govuk-frontend-jinja.git"}
gunicorn = {version = "==20.1.0", extras = ["eventlet"], ref = "1299ea9e967a61ae2edebe191082fd169b864c64", git = "https://github.com/benoitc/gunicorn.git"}
humanize = "~=4.1"
itsdangerous = "~=2.1"
jinja2 = "~=3.1"
notifications-python-client = "==6.3.0"
prometheus-client = "==0.14.1"
pyexcel = "==0.7.0"
pyexcel-io = "==0.6.6"
pyexcel-ods3 = "==0.6.1"
pyexcel-xls = "==0.7.0"
pyexcel-xlsx = "==0.6.0"
openpyxl = "==3.0.10"
pyproj = "==3.3.1"
python-dotenv = "==0.20.0"
pytz = "==2022.1"
rtreelib = "==0.2.0"
werkzeug = "~=2.3"
wtforms = "~=3.0"
newrelic = "*"
flask-talisman = "*"
notifications-utils = {editable = true, ref = "main", git = "https://github.com/GSA/notifications-utils.git"}
coverage = "*"
[dev-packages]
isort = "==5.10.1"
pytest = "==7.1.2"
pytest-env = "==0.6.2"
pytest-mock = "==3.7.0"
pytest-xdist = "==2.5.0"
beautifulsoup4 = "==4.11.1"
freezegun = "==1.2.1"
flake8 = "==4.0.1"
flake8-bugbear = "==22.4.25"
flake8-print = "==5.0.0"
moto = "==3.1.7"
requests-mock = "==1.10.0"
# used for creating manifest file locally
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
pip-audit = "*"
bandit = "*"
[requires]
python_version = "3.9"

2032
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

478
README.md
View File

@@ -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,433 +9,48 @@ 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:
- [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 [pipenv](https://pipenv.pypa.io/en/latest/)
## Local Environment Setup
1. Install Python and Node dependencies
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.
`make bootstrap`
These instructions will walk you through how to set your machine up with all of
the required tools for this project.
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
### 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
## 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.
If you are using VS Code, there are also instructions for [running inside Docker](./docs/docker-remote-containers.md)
## To test the application
@@ -451,13 +64,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 +81,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
[GSAs 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 [GSAs 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.
Were a cross-functional team of technologists with specialized experience
working across public benefits programs like Medicaid, SNAP, and unemployment
insurance.
Were 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)

View File

@@ -2,7 +2,6 @@ import os
import pathlib
from functools import partial
from time import monotonic
from urllib.parse import urlparse, urlunparse
import jinja2
from flask import (
@@ -21,6 +20,7 @@ from flask_login import LoginManager, current_user
from flask_talisman import Talisman
from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
from gds_metrics import GDSMetrics
from govuk_frontend_jinja.flask_ext import init_govuk_frontend
from itsdangerous import BadSignature
from notifications_python_client.errors import HTTPError
@@ -34,12 +34,12 @@ from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from werkzeug.exceptions import abort
from werkzeug.local import LocalProxy
from app import proxy_fix
from app import proxy_fix, webauthn_server
from app.asset_fingerprinter import asset_fingerprinter
from app.config import configs
from app.custom_auth import CustomBasicAuth
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 +54,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,
@@ -67,7 +66,7 @@ from app.formatters import (
format_notification_type,
format_number_in_pounds_as_currency,
format_thousands,
format_time_24h,
format_time,
format_yes_no,
id_safe,
iteration_count,
@@ -82,7 +81,7 @@ from app.formatters import (
square_metres_to_square_miles,
valid_phone_number,
)
from app.models.organization import Organization
from app.models.organisation import Organisation
from app.models.service import Service
from app.models.user import AnonymousUser, User
from app.navigation import (
@@ -90,56 +89,71 @@ 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
from app.notify_client.job_api_client import job_api_client
from app.notify_client.notification_api_client import notification_api_client
from app.notify_client.org_invite_api_client import org_invite_api_client
from app.notify_client.organizations_api_client import organizations_client
from app.notify_client.organisations_api_client import organisations_client
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.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.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()
talisman = Talisman()
metrics = GDSMetrics()
basic_auth = CustomBasicAuth()
# The current service attached to the request stack.
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
current_service = LocalProxy(partial(getattr, request_ctx, 'service'))
# The current organization attached to the request stack.
current_organization = LocalProxy(partial(getattr, request_ctx, "organization"))
# The current organisation attached to the request stack.
current_organisation = LocalProxy(partial(getattr, request_ctx, 'organisation'))
navigation = {
"casework_navigation": CaseworkNavigation(),
"main_navigation": MainNavigation(),
"header_navigation": HeaderNavigation(),
"org_navigation": OrgNavigation(),
"secondary_navigation": SecondaryNavigation(),
'casework_navigation': CaseworkNavigation(),
'main_navigation': MainNavigation(),
'header_navigation': HeaderNavigation(),
'org_navigation': OrgNavigation(),
}
def _csp(config):
asset_domain = config["ASSET_DOMAIN"]
logo_domain = config["LOGO_CDN_DOMAIN"]
asset_domain = config['ASSET_DOMAIN']
logo_domain = config['LOGO_CDN_DOMAIN']
return {
"default-src": ["'self'", asset_domain],
"default-src": [
"'self'",
asset_domain
],
"frame-ancestors": "'none'",
"form-action": "'self'",
"script-src": [
@@ -148,94 +162,110 @@ 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",
"https://gov-bam.nr-data.net"
],
"style-src": ["'self'", asset_domain],
"img-src": ["'self'", asset_domain, logo_domain],
"style-src": [
"'self'",
asset_domain
],
"img-src": [
"'self'",
asset_domain,
logo_domain
]
}
def create_app(application):
notify_environment = os.environ["NOTIFY_ENVIRONMENT"]
notify_environment = os.environ['NOTIFY_ENVIRONMENT']
application.config.from_object(configs[notify_environment])
asset_fingerprinter._asset_root = application.config["ASSET_PATH"]
asset_fingerprinter._asset_root = application.config['ASSET_PATH']
init_app(application)
if "extensions" not in application.jinja_options:
application.jinja_options["extensions"] = []
if 'extensions' not in application.jinja_options:
application.jinja_options['extensions'] = []
init_govuk_frontend(application)
init_jinja(application)
for client in (
# Gubbins
# Note, metrics purposefully first so we start measuring response times as early as possible before any
# other `app.before_request` handlers (introduced by any of these clients) are processed (which would
# otherwise mean we aren't measuring the full response time)
metrics,
csrf,
login_manager,
proxy_fix,
request_helper,
# API clients
api_key_api_client,
billing_api_client,
complaint_api_client,
email_branding_client,
events_api_client,
inbound_number_client,
invite_api_client,
job_api_client,
notification_api_client,
org_invite_api_client,
organizations_client,
organisations_client,
performance_dashboard_api_client,
platform_stats_api_client,
provider_client,
service_api_client,
status_api_client,
template_folder_api_client,
template_statistics_client,
upload_api_client,
user_api_client,
# External API clients
redis_client,
zendesk_client,
):
client.init_app(application)
talisman.init_app(
application,
content_security_policy=_csp(application.config),
content_security_policy_nonce_in=["style-src", "script-src"],
content_security_policy_nonce_in=['style-src', 'script-src'],
permissions_policy={
"accelerometer": "()",
"ambient-light-sensor": "()",
"autoplay": "()",
"battery": "()",
"camera": "()",
"document-domain": "()",
"geolocation": "()",
"gyroscope": "()",
"local-fonts": "()",
"magnetometer": "()",
"microphone": "()",
"midi": "()",
"payment": "()",
"screen-wake-lock": "()",
'accelerometer': '()',
'ambient-light-sensor': '()',
'autoplay': '()',
'battery': '()',
'camera': '()',
'document-domain': '()',
'geolocation': '()',
'gyroscope': '()',
'local-fonts': '()',
'magnetometer': '()',
'microphone': '()',
'midi': '()',
'payment': '()',
'screen-wake-lock': '()'
},
frame_options="deny",
force_https=(application.config["HTTP_PROTOCOL"] == "https"),
frame_options='deny',
force_https=(application.config['HTTP_PROTOCOL'] == 'https')
)
logging.init_app(application)
webauthn_server.init_app(application)
login_manager.login_view = "main.sign_in"
login_manager.login_message_category = "default"
login_manager.login_view = 'main.sign_in'
login_manager.login_message_category = 'default'
login_manager.session_protection = None
login_manager.anonymous_user = AnonymousUser
setup_basic_auth(application)
# make sure we handle unicode correctly
redis_client.redis_store.decode_responses = True
@@ -254,62 +284,33 @@ def create_app(application):
def init_app(application):
application.before_request(redirect_notify_to_beta)
application.before_request(load_service_before_request)
application.before_request(load_organization_before_request)
application.before_request(load_organisation_before_request)
application.before_request(request_helper.check_proxy_header_before_request)
application.before_request(make_session_permanent)
application.after_request(save_service_or_org_after_request)
start = len(asset_fingerprinter._filesystem_path)
font_paths = [
str(item)[start:]
for item in pathlib.Path(asset_fingerprinter._filesystem_path).glob(
"fonts/*.woff2"
)
str(item)[len(asset_fingerprinter._filesystem_path):]
for item in pathlib.Path(asset_fingerprinter._filesystem_path).glob('fonts/*.woff2')
]
@application.context_processor
def _attach_current_service():
return {"current_service": current_service}
return {'current_service': current_service}
@application.context_processor
def _attach_current_organization():
return {"current_org": current_organization}
def _attach_current_organisation():
return {'current_org': current_organisation}
@application.context_processor
def _attach_current_user():
return {"current_user": current_user}
return {'current_user': current_user}
@application.context_processor
def _nav_selected():
return navigation
@application.context_processor
def _attach_current_global_daily_messages():
global_limit = 0
remaining_global_messages = 0
if current_app:
if request.view_args:
service_id = request.view_args.get(
"service_id", session.get("service_id")
)
else:
service_id = session.get("service_id")
if service_id:
global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"]
global_messages_count = (
service_api_client.get_global_notification_count(service_id)
)
remaining_global_messages = global_limit - global_messages_count.get(
"count"
)
return {
"global_message_limit": global_limit,
"daily_global_messages_remaining": remaining_global_messages,
}
@application.before_request
def record_start_time():
g.start = monotonic()
@@ -318,15 +319,16 @@ def init_app(application):
@application.context_processor
def inject_global_template_variables():
return {
"asset_path": application.config["ASSET_PATH"],
"header_colour": application.config["HEADER_COLOUR"],
"asset_url": asset_fingerprinter.get_url,
"font_paths": font_paths,
'asset_path': application.config['ASSET_PATH'],
'header_colour': application.config['HEADER_COLOUR'],
'asset_url': asset_fingerprinter.get_url,
'font_paths': font_paths,
}
application.url_map.converters["uuid"].to_python = lambda self, value: value
application.url_map.converters["template_type"] = TemplateTypeConverter
application.url_map.converters["simple_date"] = SimpleDateTypeConverter
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
@login_manager.user_loader
@@ -338,7 +340,7 @@ def make_session_permanent():
"""
Make sessions permanent. By permanent, we mean "admin app sets when it expires". Normally the cookie would expire
whenever you close the browser. With this, the session expiry is set in `config['PERMANENT_SESSION_LIFETIME']`
(30 min) and is refreshed after every request. IE: you will be logged out after thirty minutes of inactivity.
(20 hours) and is refreshed after every request. IE: you will be logged out after twenty hours of inactivity.
We don't _need_ to set this every request (it's saved within the cookie itself under the `_permanent` flag), only
when you first log in/sign up/get invited/etc, but we do it just to be safe. For more reading, check here:
@@ -347,39 +349,22 @@ def make_session_permanent():
session.permanent = True
def create_beta_url(url):
url_created = urlparse(url)
url_list = list(url_created)
url_list[1] = "beta.notify.gov"
url_for_redirect = urlunparse(url_list)
return url_for_redirect
def redirect_notify_to_beta():
if (
current_app.config["NOTIFY_ENVIRONMENT"] == "production"
and "beta.notify.gov" not in request.url
):
url_to_beta = create_beta_url(request.url)
return redirect(url_to_beta, 302)
def load_service_before_request():
if "/static/" in request.url:
if '/static/' in request.url:
request_ctx.service = None
return
if request_ctx is not None:
request_ctx.service = None
if request.view_args:
service_id = request.view_args.get("service_id", session.get("service_id"))
service_id = request.view_args.get('service_id', session.get('service_id'))
else:
service_id = session.get("service_id")
service_id = session.get('service_id')
if service_id:
try:
request_ctx.service = Service(
service_api_client.get_service(service_id)["data"]
service_api_client.get_service(service_id)['data']
)
except HTTPError as exc:
# if service id isn't real, then 404 rather than 500ing later because we expect service to be set
@@ -389,19 +374,19 @@ def load_service_before_request():
raise
def load_organization_before_request():
if "/static/" in request.url:
request_ctx.organization = None
def load_organisation_before_request():
if '/static/' in request.url:
request_ctx.organisation = None
return
if request_ctx is not None:
request_ctx.organization = None
request_ctx.organisation = None
if request.view_args:
org_id = request.view_args.get("org_id")
org_id = request.view_args.get('org_id')
if org_id:
try:
request_ctx.organization = Organization.from_id(org_id)
request_ctx.organisation = Organisation.from_id(org_id)
except HTTPError as exc:
# if org id isn't real, then 404 rather than 500ing later because we expect org to be set
if exc.status_code == 404:
@@ -412,19 +397,15 @@ def load_organization_before_request():
def save_service_or_org_after_request(response):
# Only save the current session if the request is 200
service_id = (
request.view_args.get("service_id", None) if request.view_args else None
)
organization_id = (
request.view_args.get("org_id", None) if request.view_args else None
)
service_id = request.view_args.get('service_id', None) if request.view_args else None
organisation_id = request.view_args.get('org_id', None) if request.view_args else None
if response.status_code == 200:
if service_id:
session["service_id"] = service_id
session["organization_id"] = None
elif organization_id:
session["service_id"] = None
session["organization_id"] = organization_id
session['service_id'] = service_id
session['organisation_id'] = None
elif organisation_id:
session['service_id'] = None
session['organisation_id'] = organisation_id
return response
@@ -432,38 +413,32 @@ def register_errorhandlers(application): # noqa (C901 too complex)
def _error_response(error_code, error_page_template=None):
if error_page_template is None:
error_page_template = error_code
return make_response(
render_template("error/{0}.html".format(error_page_template)), error_code
)
return make_response(render_template("error/{0}.html".format(error_page_template)), error_code)
@application.errorhandler(HTTPError)
def render_http_error(error):
application.logger.warning(
"API {} failed with status {} message {}".format(
error.response.url if error.response else "unknown",
error.status_code,
error.message,
)
)
application.logger.warning("API {} failed with status {} message {}".format(
error.response.url if error.response else 'unknown',
error.status_code,
error.message
))
error_code = error.status_code
if error_code not in [401, 404, 403, 410]:
# probably a 500 or 503.
# it might be a 400, which we should handle as if it's an internal server error. If the API might
# legitimately return a 400, we should handle that within the view or the client that calls it.
application.logger.exception(
"API {} failed with status {} message {}".format(
error.response.url if error.response else "unknown",
error.status_code,
error.message,
)
)
application.logger.exception("API {} failed with status {} message {}".format(
error.response.url if error.response else 'unknown',
error.status_code,
error.message
))
error_code = 500
return _error_response(error_code)
@application.errorhandler(400)
def handle_client_error(error):
# This is tripped if we call `abort(400)`.
application.logger.exception("Unhandled 400 client error")
application.logger.exception('Unhandled 400 client error')
return _error_response(400, error_page_template=500)
@application.errorhandler(410)
@@ -485,24 +460,23 @@ def register_errorhandlers(application): # noqa (C901 too complex)
@application.errorhandler(BadSignature)
def handle_bad_token(error):
# if someone has a malformed token
flash("Theres something wrong with the link youve used.")
flash('Theres something wrong with the link youve used.')
return _error_response(404)
@application.errorhandler(CSRFError)
def handle_csrf(reason):
application.logger.warning("csrf.error_message: {}".format(reason))
application.logger.warning('csrf.error_message: {}'.format(reason))
if "user_id" not in session:
if 'user_id' not in session:
application.logger.warning(
"csrf.session_expired: Redirecting user to log in page"
u'csrf.session_expired: Redirecting user to log in page'
)
return application.login_manager.unauthorized()
application.logger.warning(
"csrf.invalid_token: Aborting request, user_id: {user_id}",
extra={"user_id": session["user_id"]},
)
u'csrf.invalid_token: Aborting request, user_id: {user_id}',
extra={'user_id': session['user_id']})
return _error_response(400, error_page_template=500)
@@ -521,14 +495,14 @@ def register_errorhandlers(application): # noqa (C901 too complex)
@application.errorhandler(InviteTokenError)
def handle_bad_invite_token(error):
flash(str(error))
return redirect(url_for("main.sign_in"))
return redirect(url_for('main.sign_in'))
@application.errorhandler(500)
@application.errorhandler(Exception)
def handle_bad_request(error):
current_app.logger.exception(error)
# We want the Flask in browser stacktrace
if current_app.config.get("DEBUG", None):
if current_app.config.get('DEBUG', None):
raise error
return _error_response(500)
@@ -549,7 +523,7 @@ def add_template_filters(application):
format_datetime_24h,
format_datetime_normal,
format_datetime_short,
format_datetime_short_america,
format_time,
valid_phone_number,
linkable_name,
format_date,
@@ -562,7 +536,6 @@ def add_template_filters(application):
format_day_of_week,
format_delta,
format_delta_days,
format_time_24h,
format_notification_status,
format_notification_type,
format_notification_status_as_time,
@@ -587,15 +560,18 @@ def add_template_filters(application):
format_mobile_network,
format_yes_no,
square_metres_to_square_miles,
convert_markdown_template,
]:
application.add_template_filter(fn)
def init_jinja(application):
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
template_folders = [
os.path.join(repo_root, "app/templates"),
os.path.join(repo_root, 'app/templates'),
]
jinja_loader = jinja2.FileSystemLoader(template_folders)
application.jinja_loader = jinja_loader
def setup_basic_auth(application):
application.basic_auth = CustomBasicAuth(application)

View File

@@ -3,23 +3,23 @@ import hashlib
class AssetFingerprinter(object):
"""
Get a unique hash for an asset file, so that it doesn't stay cached
when it changes
Get a unique hash for an asset file, so that it doesn't stay cached
when it changes
Usage:
Usage:
in the application
template_data.asset_fingerprinter = AssetFingerprinter()
in the application
template_data.asset_fingerprinter = AssetFingerprinter()
where template data is how you pass variables to every template.
where template data is how you pass variables to every template.
in template.html:
{{ asset_fingerprinter.get_url('stylesheets/application.css') }}
in template.html:
{{ asset_fingerprinter.get_url('stylesheets/application.css') }}
* 'app/static' is assumed to be the root for all asset files
* 'app/static' is assumed to be the root for all asset files
"""
def __init__(self, asset_root="/static/", filesystem_path="app/static/"):
def __init__(self, asset_root='/static/', filesystem_path='app/static/'):
self._cache = {}
self._asset_root = asset_root
self._filesystem_path = filesystem_path
@@ -29,10 +29,10 @@ class AssetFingerprinter(object):
return self._asset_root + asset_path
if asset_path not in self._cache:
self._cache[asset_path] = (
self._asset_root
+ asset_path
+ "?"
+ self.get_asset_fingerprint(self._filesystem_path + asset_path)
self._asset_root +
asset_path +
'?' +
self.get_asset_fingerprint(self._filesystem_path + asset_path)
)
return self._cache[asset_path]
@@ -42,7 +42,7 @@ class AssetFingerprinter(object):
).hexdigest()
def get_asset_file_contents(self, asset_file_path):
with open(asset_file_path, "rb") as asset_file:
with open(asset_file_path, 'rb') as asset_file:
contents = asset_file.read()
return contents

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
<!DOCTYPE html>
<html lang="en" class="usa-template">
<html lang="en" class="govuk-template ">
<head>
<meta charset="utf-8" />
<title>
Sorry, there is a problem with the service Notify.gov
Sorry, there is a problem with the service U.S. Notify
</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#0b0c0c" />
@@ -12,43 +12,50 @@
<link rel="shortcut icon" sizes="16x16 32x32 48x48" href="/static/images/favicon.ico?de7abc5226925203ac10b0a4a94af949"
type="image/x-icon" />
<link rel="mask-icon" href="/static/images/usa-mask-icon.svg?2afd125b1db96109388d4a0727312f3e" color="#F0F0F0">
<link rel="mask-icon" href="/static/images/govuk-mask-icon.svg?2afd125b1db96109388d4a0727312f3e" color="#0b0c0c">
<link rel="apple-touch-icon" sizes="180x180"
href="/static/images/apple-touch-icon.png?a0f7e1b728a42016b247dc54ee40d055">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
<link rel="manifest" href="/static/images/site.webmanifest">
href="/static/images/govuk-apple-touch-icon-180x180.png?a0f7e1b728a42016b247dc54ee40d055">
<link rel="apple-touch-icon" sizes="167x167"
href="/static/images/govuk-apple-touch-icon-167x167.png?f636cb7d471fc6239d2241d75cbdabcb">
<link rel="apple-touch-icon" sizes="152x152"
href="/static/images/govuk-apple-touch-icon-152x152.png?40846d46aa37232e2b35065769ce567c">
<link rel="apple-touch-icon" href="/static/images/govuk-apple-touch-icon.png?3c45daa1f1e716458e4da954b973002e">
<link rel="stylesheet" media="screen" href="/static/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="print" href="/static/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
<style>
.govuk-header__container {
border-color: #005EA5
}
</style>
<meta property="og:image" content="/static/images/usa-opengraph-image.png">
<meta property="og:image" content="/static/images/govuk-opengraph-image.png">
</head>
<body class="usa-template__body">
<a href="#main-content" class="usa-skip-link">Skip to main content</a>
<header class="usa-header " role="banner" data-module="header">
<div class="usa-header__container usa-width-container">
<body class="govuk-template__body ">
<a href="#main-content" class="govuk-skip-link">Skip to main content</a>
<header class="govuk-header " role="banner" data-module="header">
<div class="govuk-header__container govuk-width-container">
<div class="usa-header__logo">
<a href="/accounts-or-dashboard" class="usa-header__link usa-header__link--homepage">
<span class="usa-header__logotype">
<div class="govuk-header__logo">
<a href="/accounts-or-dashboard" class="govuk-header__link govuk-header__link--homepage">
<span class="govuk-header__logotype">
<svg role="presentation" focusable="false" class="usa-header__logotype"
<svg role="presentation" focusable="false" class="govuk-header__logotype"
xmlns="http://www.w3.org/2000/svg" viewbox="0 0 132 97" height="32" width="36">
<path fill="currentColor" fill-rule="evenodd"
d="M25 30.2c3.5 1.5 7.7-.2 9.1-3.7 1.5-3.6-.2-7.8-3.9-9.2-3.6-1.4-7.6.3-9.1 3.9-1.4 3.5.3 7.5 3.9 9zM9 39.5c3.6 1.5 7.8-.2 9.2-3.7 1.5-3.6-.2-7.8-3.9-9.1-3.6-1.5-7.6.2-9.1 3.8-1.4 3.5.3 7.5 3.8 9zM4.4 57.2c3.5 1.5 7.7-.2 9.1-3.8 1.5-3.6-.2-7.7-3.9-9.1-3.5-1.5-7.6.3-9.1 3.8-1.4 3.5.3 7.6 3.9 9.1zm38.3-21.4c3.5 1.5 7.7-.2 9.1-3.8 1.5-3.6-.2-7.7-3.9-9.1-3.6-1.5-7.6.3-9.1 3.8-1.3 3.6.4 7.7 3.9 9.1zm64.4-5.6c-3.6 1.5-7.8-.2-9.1-3.7-1.5-3.6.2-7.8 3.8-9.2 3.6-1.4 7.7.3 9.2 3.9 1.3 3.5-.4 7.5-3.9 9zm15.9 9.3c-3.6 1.5-7.7-.2-9.1-3.7-1.5-3.6.2-7.8 3.7-9.1 3.6-1.5 7.7.2 9.2 3.8 1.5 3.5-.3 7.5-3.8 9zm4.7 17.7c-3.6 1.5-7.8-.2-9.2-3.8-1.5-3.6.2-7.7 3.9-9.1 3.6-1.5 7.7.3 9.2 3.8 1.3 3.5-.4 7.6-3.9 9.1zM89.3 35.8c-3.6 1.5-7.8-.2-9.2-3.8-1.4-3.6.2-7.7 3.9-9.1 3.6-1.5 7.7.3 9.2 3.8 1.4 3.6-.3 7.7-3.9 9.1zM69.7 17.7l8.9 4.7V9.3l-8.9 2.8c-.2-.3-.5-.6-.9-.9L72.4 0H59.6l3.5 11.2c-.3.3-.6.5-.9.9l-8.8-2.8v13.1l8.8-4.7c.3.3.6.7.9.9l-5 15.4v.1c-.2.8-.4 1.6-.4 2.4 0 4.1 3.1 7.5 7 8.1h.2c.3 0 .7.1 1 .1.4 0 .7 0 1-.1h.2c4-.6 7.1-4.1 7.1-8.1 0-.8-.1-1.7-.4-2.4V34l-5.1-15.4c.4-.2.7-.6 1-.9zM66 92.8c16.9 0 32.8 1.1 47.1 3.2 4-16.9 8.9-26.7 14-33.5l-9.6-3.4c1 4.9 1.1 7.2 0 10.2-1.5-1.4-3-4.3-4.2-8.7L108.6 76c2.8-2 5-3.2 7.5-3.3-4.4 9.4-10 11.9-13.6 11.2-4.3-.8-6.3-4.6-5.6-7.9 1-4.7 5.7-5.9 8-.5 4.3-8.7-3-11.4-7.6-8.8 7.1-7.2 7.9-13.5 2.1-21.1-8 6.1-8.1 12.3-4.5 20.8-4.7-5.4-12.1-2.5-9.5 6.2 3.4-5.2 7.9-2 7.2 3.1-.6 4.3-6.4 7.8-13.5 7.2-10.3-.9-10.9-8-11.2-13.8 2.5-.5 7.1 1.8 11 7.3L80.2 60c-4.1 4.4-8 5.3-12.3 5.4 1.4-4.4 8-11.6 8-11.6H55.5s6.4 7.2 7.9 11.6c-4.2-.1-8-1-12.3-5.4l1.4 16.4c3.9-5.5 8.5-7.7 10.9-7.3-.3 5.8-.9 12.8-11.1 13.8-7.2.6-12.9-2.9-13.5-7.2-.7-5 3.8-8.3 7.1-3.1 2.7-8.7-4.6-11.6-9.4-6.2 3.7-8.5 3.6-14.7-4.6-20.8-5.8 7.6-5 13.9 2.2 21.1-4.7-2.6-11.9.1-7.7 8.8 2.3-5.5 7.1-4.2 8.1.5.7 3.3-1.3 7.1-5.7 7.9-3.5.7-9-1.8-13.5-11.2 2.5.1 4.7 1.3 7.5 3.3l-4.7-15.4c-1.2 4.4-2.7 7.2-4.3 8.7-1.1-3-.9-5.3 0-10.2l-9.5 3.4c5 6.9 9.9 16.7 14 33.5 14.8-2.1 30.8-3.2 47.7-3.2z">
</path>
<image src="https://images.emojiterra.com/twitter/v14.0/512px/1f1fa-1f1f8.png" xlink:href=""
class="usa-header__logotype-fallback-image" width="36" height="32"></image>
class="govuk-header__logotype-fallback-image" width="36" height="32"></image>
</svg>
<span class="usa-header__logotype-text">
<span class="govuk-header__logotype-text">
US
</span>
</span>
<span class="usa-header__product-name">
<span class="govuk-header__product-name">
Notify
</span>
@@ -58,18 +65,18 @@
</div>
</header>
<div class="usa-width-container">
<main class="usa-main-wrapper" id="main-content" role="main">
<div class="grid-row">
<div class="grid-col-12">
<h1 class="usa-heading-l">
<div class="govuk-width-container">
<main class="govuk-main-wrapper govuk-!-padding-top-0 govuk-!-padding-bottom-12" id="main-content" role="main">
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<h1 class="govuk-heading-l govuk-!-padding-top-7">
Sorry, there is a problem with the service
</h1>
<p class="usa-body">
<p class="govuk-body">
Try again later.
</p>
<p class="usa-body">
You can check our <a class="usa-link"
<p class="govuk-body">
You can check our <a class="govuk-link govuk-link--no-visited-state"
href="https://status.notifications.service.gov.uk/">system status</a> page to see if there are any known
issues.<br />To report a problem, email <a
href="mailto:gov-uk-notify-support@digital.cabinet-office.gov.uk">gov-uk-notify-support@digital.cabinet-office.gov.uk</a>
@@ -78,25 +85,25 @@
</main>
</div>
<footer class="usa-footer " role="contentinfo">
<div class="usa-width-container ">
<div class="usa-footer__meta">
<div class="usa-footer__meta-item usa-footer__meta-item--grow">
<footer class="govuk-footer " role="contentinfo">
<div class="govuk-width-container ">
<div class="govuk-footer__meta">
<div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
<h2 class="usa-sr-only">Support links</h2>
<div class="usa-footer__meta-custom">
<h2 class="govuk-visually-hidden">Support links</h2>
<div class="govuk-footer__meta-custom">
Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions"
class="usa-footer__link">Technology Transformation Services</a>
class="govuk-footer__link">Technology Transformation Service</a>
</div>
<svg role="presentation" focusable="false" class="usa-footer__licence-logo"
<svg role="presentation" focusable="false" class="govuk-footer__licence-logo"
xmlns="http://www.w3.org/2000/svg" viewbox="0 0 483.2 195.7" height="17" width="41">
<path fill="currentColor"
d="M421.5 142.8V.1l-50.7 32.3v161.1h112.4v-50.7zm-122.3-9.6A47.12 47.12 0 0 1 221 97.8c0-26 21.1-47.1 47.1-47.1 16.7 0 31.4 8.7 39.7 21.8l42.7-27.2A97.63 97.63 0 0 0 268.1 0c-36.5 0-68.3 20.1-85.1 49.7A98 98 0 0 0 97.8 0C43.9 0 0 43.9 0 97.8s43.9 97.8 97.8 97.8c36.5 0 68.3-20.1 85.1-49.7a97.76 97.76 0 0 0 149.6 25.4l19.4 22.2h3v-87.8h-80l24.3 27.5zM97.8 145c-26 0-47.1-21.1-47.1-47.1s21.1-47.1 47.1-47.1 47.2 21 47.2 47S123.8 145 97.8 145" />
</svg>
<span class="usa-footer__licence-description">
<span class="govuk-footer__licence-description">
All content is available under the
<a class="usa-footer__link"
<a class="govuk-footer__link"
href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" rel="license">Open
Government Licence v3.0</a>, except where otherwise stated
</span>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 105 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 133 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -1,4 +0,0 @@
<svg width="680" height="680" viewBox="0 0 680 680" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M672 211H332V161L672 161V211ZM672 294H332V244H672V294ZM672 377H332V327H672V377ZM672 410V460H8V410H672ZM672 493V543H8V493H672Z" fill="#D83933"/>
<path d="M8 161H299V377H8V161Z" fill="#3C3B6E"/>
</svg>

Before

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="719px" height="720px" viewBox="0 0 129.996 130.238" enable-background="new 0 0 129.996 130.238"
xml:space="preserve">
<rect fill="#00538E" width="129.996" height="130.238"/>
<path fill="#FFFFFF" d="M78.611,96.076c0,9.138-7.175,12.953-15.451,12.688c-2.938-0.095-5.799-0.838-8.916-1.855l0.707-6.578
c2.638,1.086,6.057,2.263,8.92,2.219c3.74-0.058,7.477-1.794,7.477-6.124c0-3.335-2.67-4.976-5.252-6.283
c-5.709-2.896-12.275-4.907-12.275-12.998c0-7.65,6.084-11.64,13.625-11.64c2.741,0,6.014,0.477,9.299,1.641l-0.985,6.478
c-3.577-1.403-5.14-1.901-8.078-1.901c-3.253,0-6.597,1.143-6.597,4.896c0,2.933,3.01,4.456,5.248,5.573
C72.398,85.218,78.611,87.703,78.611,96.076"/>
<path fill="#FFFFFF" d="M44.561,106.8c-2.632,0.901-6.662,1.964-13.092,1.964c-13.195,0-22.027-8.529-22.027-21.805
c0-12.882,9.375-21.455,22.086-21.455c6.036,0,8.727,0.896,12.541,2.212l-0.574,7.216c-3.51-2.294-6.498-3.211-11.674-3.211
c-9.09,0-15.158,6.578-15.115,15.412c0.043,9.506,6.648,15.416,14.822,15.416c2.437,0,4.508-0.178,5.996-0.569V91.15h-7.861v-6.103
h14.898V106.8"/>
<polyline fill="#FFFFFF" points="102.041,64.587 112.063,86.918 105.08,86.918 102.041,79.497 98.996,86.918 92.014,86.918
102.041,64.587 "/>
<polyline fill="#FFFFFF" points="112.178,87.179 121.507,108.029 114.176,108.029 107.203,91.839 112.178,87.179 "/>
<polyline fill="#FFFFFF" points="91.9,87.183 96.873,91.841 93.199,100.24 102.027,95.611 107.109,99.525 89.727,108.031
82.57,108.031 91.9,87.183 "/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,69 @@
(function (window) {
"use strict";
window.GOVUK = window.GOVUK || {};
// Stripped-down wrapper for Google Analytics, based on:
// https://github.com/alphagov/static/blob/master/doc/analytics.md
const Analytics = function (config) {
window.ga('create', {
'trackingId': config.trackingId,
'cookieDomain': config.cookieDomain,
'cookieExpires': config.expires * 24 * 60 * 60
});
window.ga('set', 'anonymizeIp', config.anonymizeIp);
window.ga('set', 'allowAdFeatures', config.allowAdFeatures);
window.ga('set', 'transport', config.transport);
window.ga('set', 'title', 'U.S. Notify');
};
Analytics.load = function () {
/* jshint ignore:start */
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
/* jshint ignore:end */
};
Analytics.prototype.trackPageview = function (path, title, options) {
// strip UUIDs
const page = `${window.location.pathname}${window.location.search}`.replace(
/[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}/g, '…'
);
window.ga('send', 'pageview', page);
};
// https://developers.google.com/analytics/devguides/collection/analyticsjs/events
Analytics.prototype.trackEvent = function (category, action, options) {
options = options || {};
var evt = {
eventCategory: category,
eventAction: action
};
if (options.label) {
evt.eventLabel = options.label;
delete options.label;
}
if (typeof options === 'object') {
$.extend(evt, options);
}
window.ga('send', 'event', evt);
};
window.GOVUK.Analytics = Analytics;
})(window);

View File

@@ -0,0 +1,40 @@
(function (window) {
"use strict";
window.GOVUK = window.GOVUK || {};
const trackingId = 'UA-75215134-1';
// Disable analytics by default
window[`ga-disable-${trackingId}`] = true;
const initAnalytics = function () {
// guard against being called more than once
if (!('analytics' in window.GOVUK)) {
window[`ga-disable-${trackingId}`] = false;
// Load Google Analytics libraries
window.GOVUK.Analytics.load();
// Configure profiles and make interface public
// for custom dimensions, virtual pageviews and events
window.GOVUK.analytics = new GOVUK.Analytics({
trackingId: trackingId,
cookieDomain: 'auto',
anonymizeIp: true,
allowAdFeatures: false,
transport: 'beacon',
expires: 365
});
// Track initial pageview
window.GOVUK.analytics.trackPageview();
}
};
window.GOVUK.initAnalytics = initAnalytics;
})(window);

View File

@@ -0,0 +1,74 @@
(function (window) {
"use strict";
window.GOVUK.Modules.AuthenticateSecurityKey = function () {
this.start = function (component) {
$(component)
.on('click', function (event) {
event.preventDefault();
// hide any existing error prompt
window.GOVUK.ErrorBanner.hideBanner();
fetch('/webauthn/authenticate')
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.arrayBuffer();
})
.then(data => {
var options = window.CBOR.decode(data);
// triggers browser dialogue to login with authenticator
return window.navigator.credentials.get(options);
})
.then(credential => {
const currentURL = new URL(window.location.href);
// create authenticateURL from admin hostname plus /webauthn/authenticate path
const authenticateURL = new URL('/webauthn/authenticate', window.location.href);
const nextUrl = currentURL.searchParams.get('next');
if (nextUrl) {
// takes nextUrl from the query string on the current browser URL
// (which should be /two-factor-webauthn) and pass it through to
// the POST. put it in a query string so it's consistent with how
// the other login flows manage it
authenticateURL.searchParams.set('next', nextUrl);
}
return fetch(authenticateURL, {
method: 'POST',
headers: { 'X-CSRFToken': component.data('csrfToken') },
body: window.CBOR.encode({
credentialId: new Uint8Array(credential.rawId),
authenticatorData: new Uint8Array(credential.response.authenticatorData),
signature: new Uint8Array(credential.response.signature),
clientDataJSON: new Uint8Array(credential.response.clientDataJSON),
})
});
})
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.arrayBuffer();
})
.then(cbor => {
return Promise.resolve(window.CBOR.decode(cbor));
})
.then(data => {
window.location.assign(data.redirect_url);
})
.catch(error => {
console.error(error);
// some browsers will show an error dialogue for some
// errors; to be safe we always display an error message on the page.
window.GOVUK.ErrorBanner.showBanner();
});
});
};
};
}) (window);

View File

@@ -20,7 +20,7 @@
}[field] || `No ${field}s`)
};
Summary.prototype.addContent = function() {
const $hint = this.module.$formGroup.find('.usa-hint');
const $hint = this.module.$formGroup.find('.govuk-hint');
this.$text = $(`<p class="selection-summary__text" />`);
if (this.fieldLabel === 'folder') { this.$text.addClass('selection-summary__text--folders'); }
@@ -56,7 +56,7 @@
}
Footer.prototype.buttonContent = {
change: (fieldLabel) => `Choose ${fieldLabel}s`,
done: (fieldLabel) => `Done<span class="usa-sr-only"> choosing ${fieldLabel}s</span>`
done: (fieldLabel) => `Done<span class="govuk-visually-hidden"> choosing ${fieldLabel}s</span>`
};
Footer.prototype.getEl = function (expanded) {
const buttonState = expanded ? 'done' : 'change';
@@ -90,7 +90,7 @@
};
CollapsibleCheckboxes.prototype.start = function(component) {
this.$component = $(component);
this.$formGroup = this.$component.find('.usa-form-group').first();
this.$formGroup = this.$component.find('.govuk-form-group').first();
this.$fieldset = this.$formGroup.find('fieldset').first();
this.$checkboxes = this.$fieldset.find('input[type=checkbox]');
this.fieldLabel = this.$component.data('fieldLabel');
@@ -122,7 +122,7 @@
this.$heading = $(`<h${headingLevel} class="heading-small">${this.legendText}</h${headingLevel}>`);
this.$fieldset.before(this.$heading);
this.$fieldset.find('legend').addClass('usa-sr-only');
this.$fieldset.find('legend').addClass('govuk-visually-hidden');
};
CollapsibleCheckboxes.prototype.expand = function(e) {
if (e !== undefined) { e.preventDefault(); }

View File

@@ -10,7 +10,7 @@
this.$input = $(component);
this.$input.closest('.usa-form-group').append(
this.$input.closest('.govuk-form-group').append(
this.$preview = $('<span class="textbox-colour-preview"></span>')
);

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

View 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 ? 'Youve 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);

View File

@@ -0,0 +1,84 @@
window.GOVUK = window.GOVUK || {};
window.GOVUK.Modules = window.GOVUK.Modules || {};
(function (Modules) {
function CookieSettings () {}
CookieSettings.prototype.start = function ($module) {
this.$module = $module[0];
this.$module.submitSettingsForm = this.submitSettingsForm.bind(this);
document.querySelector('form[data-module=cookie-settings]')
.addEventListener('submit', this.$module.submitSettingsForm);
this.setInitialFormValues();
};
CookieSettings.prototype.setInitialFormValues = function () {
var currentConsentCookie = window.GOVUK.getConsentCookie('consent');
if (!currentConsentCookie) { return; }
var radioButton;
if (currentConsentCookie.analytics) {
radioButton = document.querySelector('input[name=cookies-analytics][value=on]');
} else {
radioButton = document.querySelector('input[name=cookies-analytics][value=off]');
}
radioButton.checked = true;
};
CookieSettings.prototype.submitSettingsForm = function (event) {
event.preventDefault();
var formInputs = event.target.querySelectorAll("input[name=cookies-analytics]");
var options = {};
for ( var i = 0; i < formInputs.length; i++ ) {
var input = formInputs[i];
if (input.checked) {
var value = input.value === "on" ? true : false;
options.analytics = value;
break;
}
}
window.GOVUK.setConsentCookie(options);
this.showConfirmationMessage();
if(window.GOVUK.hasConsentFor('analytics')) {
window.GOVUK.initAnalytics();
}
return false;
};
CookieSettings.prototype.showConfirmationMessage = function () {
var confirmationMessage = document.querySelector('div[data-cookie-confirmation]');
var previousPageLink = document.querySelector('.cookie-settings__prev-page');
var referrer = CookieSettings.prototype.getReferrerLink();
document.body.scrollTop = document.documentElement.scrollTop = 0;
if (referrer && referrer !== document.location.pathname) {
previousPageLink.href = referrer;
previousPageLink.style.display = "block";
} else {
previousPageLink.style.display = "none";
}
confirmationMessage.style.display = "block";
};
CookieSettings.prototype.getReferrerLink = function () {
return document.referrer ? new URL(document.referrer).pathname : false;
};
Modules.CookieSettings = CookieSettings;
})(window.GOVUK.Modules);

View File

@@ -7,20 +7,20 @@
const states = {
'valueVisible': (options) => `
<span class="copy-to-clipboard__value margin-bottom-1">${options.valueLabel ? '<span class="usa-sr-only">' + options.thing + ': </span>' : ''}${options.value}</span>
<span class="copy-to-clipboard__notice" aria-live="assertive">
<span class="copy-to-clipboard__value">${options.valueLabel ? '<span class="govuk-visually-hidden">' + options.thing + ': </span>' : ''}${options.value}</span>
<span class="copy-to-clipboard__notice govuk-visually-hidden" aria-live="assertive">
${options.onload ? '' : options.thing + ' returned to page, press button to copy to clipboard'}
</span>
<button class="usa-button usa-button--outline copy-to-clipboard__button--copy">
Copy ${options.thing} to clipboard${options.name ? '<span class="usa-sr-only"> for ' + options.name + '</span>' : ''}
<button class="govuk-button govuk-button--secondary copy-to-clipboard__button--copy">
Copy ${options.thing} to clipboard${options.name ? '<span class="govuk-visually-hidden"> for ' + options.name + '</span>' : ''}
</button>
`,
'valueCopied': (options) => `
<span class="copy-to-clipboard__notice" aria-live="assertive">
<span class="usa-sr-only">${options.thing} </span>Copied to clipboard<span class="usa-sr-only">, press button to show in page</span>
<span class="govuk-visually-hidden">${options.thing} </span>Copied to clipboard<span class="govuk-visually-hidden">, press button to show in page</span>
</span>
<button class="usa-button copy-to-clipboard__button--show">
Show ${options.thing}${options.name ? '<span class="usa-sr-only"> for ' + options.name + '</span>' : ''}
<button class="govuk-button govuk-button--secondary copy-to-clipboard__button--show">
Show ${options.thing}${options.name ? '<span class="govuk-visually-hidden"> for ' + options.name + '</span>' : ''}
</button>
`
};
@@ -31,7 +31,7 @@
let prefixIndex = -1;
childNodes.forEach((el, idx) => {
if ((el.nodeType === 1) && el.classList.contains('usa-sr-only')) {
if ((el.nodeType === 1) && el.classList.contains('govuk-visually-hidden')) {
prefixIndex = idx;
}
});

View File

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

View File

@@ -9,8 +9,9 @@
This may behave in unexpected ways if you have more than one element with the `banner-dangerous` class on your page.
*/
window.GOVUK.ErrorBanner = {
hideBanner: () => $('.banner-dangerous').addClass('display-none'),
hideBanner: () => $('.banner-dangerous').addClass('govuk-!-display-none'),
showBanner: () => $('.banner-dangerous')
.removeClass('display-none')
.removeClass('govuk-!-display-none')
.trigger('focus'),
};
})(window);

View File

@@ -6,7 +6,7 @@
this.submit = () => this.$form.trigger('submit');
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
<a href="" class='govuk-button govuk-button--warning govuk-!-margin-right-1'>Cancel upload</a>
`);
this.start = function(component) {
@@ -15,9 +15,8 @@
// The label gets styled like a button and is used to hide the native file upload control. This is so that
// users see a button that looks like the others on the site.
this.$form.find('label.file-upload-button').addClass('govuk-button govuk-!-margin-right-1');
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
// Clear the form if the user navigates back to the page
$(window).on("pageshow", () => this.$form[0].reset());

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

View File

@@ -28,7 +28,7 @@
ListEntry.prototype.entryTemplate = Hogan.compile(
'<div class="list-entry">' +
'<label for="{{{id}}}" class="govuk-input--numbered__label">' +
'<span class="usa-sr-only">{{listItemName}} number </span>{{number}}.' +
'<span class="govuk-visually-hidden">{{listItemName}} number </span>{{number}}.' +
'</label>' +
'<input' +
' name="{{name}}"' +
@@ -38,7 +38,7 @@
'/>' +
'{{#button}}' +
'<button type="button" class="govuk-button govuk-button--secondary input-list__button--remove">' +
'Remove<span class="usa-sr-only"> {{listItemName}} number {{number}}</span>' +
'Remove<span class="govuk-visually-hidden"> {{listItemName}} number {{number}}</span>' +
'</button>' +
'{{/button}}' +
'</div>'

View File

@@ -1,13 +1,25 @@
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());
$(() => GOVUK.stickAtTopWhenScrolling.init());
$(() => GOVUK.stickAtBottomWhenScrolling.init());
var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init();
$(() => GOVUK.modules.start());
$(() => $('.error-message, .usa-error-message').eq(0).parent('label').next('input').trigger('focus'));
$(() => $('.error-message, .govuk-error-message').eq(0).parent('label').next('input').trigger('focus'));
$(() => $('.banner-dangerous').eq(0).trigger('focus'));
$(() => $('.govuk-header__container').on('click', function() {
$(this).css('border-color', '#005ea5');

View File

@@ -0,0 +1,36 @@
(function (global) {
'use strict';
$ = global.jQuery;
let branding_style = $('.govuk-radios__item 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);

View File

@@ -9,57 +9,57 @@
let states = {
'initial': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
<div class="radio-select__column">
<div class="govuk-radios__item">
<input class="govuk-radios__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="govuk-label govuk-radios__label" for="{{name}}-0">Now</label>
</div>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
<div class="radio-select__column">
{{#categories}}
<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='{{.}}' />
<input type='button' class='govuk-button govuk-button--secondary radio-select__button--category' aria-expanded="false" value='{{.}}' />
{{/categories}}
</div>
`),
'choose': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
<div class="radio-select__column">
<div class="govuk-radios__item">
<input class="govuk-radios__input" checked="checked" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="govuk-label govuk-radios__label" for="{{name}}-0">Now</label>
</div>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
<div class="radio-select__column">
{{#choices}}
<div class="usa-radio js-option">
<input class="usa-radio__input" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
<div class="govuk-radios__item js-option">
<input class="govuk-radios__input" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
<label class="govuk-label govuk-radios__label" for="{{id}}">{{label}}</label>
</div>
{{/choices}}
<input type='button' class='usa-button usa-button--outline radio-select__button--done' aria-expanded='true' value='Done' />
<input type='button' class='govuk-button govuk-button--secondary radio-select__button--done' aria-expanded='true' value='Done' />
</div>
`),
'chosen': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="usa-radio__label" for="{{name}}-0">Now</label>
<div class="radio-select__column">
<div class="govuk-radios__item">
<input class="govuk-radios__input" id="{{name}}-0" name="{{name}}" type="radio" value="">
<label class="govuk-label govuk-radios__label" for="{{name}}-0">Now</label>
</div>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
<div class="radio-select__column">
{{#choices}}
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
<label class="usa-radio__label" for="{{id}}">{{label}}</label>
<div class="govuk-radios__item">
<input class="govuk-radios__input" checked="checked" type="radio" value="{{value}}" id="{{id}}" name="{{name}}" />
<label class="govuk-label govuk-radios__label" for="{{id}}">{{label}}</label>
</div>
{{/choices}}
</div>
<div class="radio-select__column margin-y-2">
<input type='button' class='usa-button usa-button--outline radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
<div class="radio-select__column">
<input type='button' class='govuk-button govuk-button--secondary radio-select__button--reset' aria-expanded='false' value='Choose a different time' />
</div>
`)
};

View File

@@ -0,0 +1,58 @@
(function(window) {
"use strict";
window.GOVUK.Modules.RegisterSecurityKey = function() {
this.start = function(component) {
$(component)
.on('click', function(event) {
event.preventDefault();
// hide any existing error prompt
window.GOVUK.ErrorBanner.hideBanner();
fetch('/webauthn/register')
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response.arrayBuffer();
})
.then((data) => {
var options = window.CBOR.decode(data);
// triggers browser dialogue to select authenticator
return window.navigator.credentials.create(options);
})
.then((credential) => {
return postWebAuthnCreateResponse(
credential.response, component.data('csrfToken')
);
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
window.location.reload();
})
.catch((error) => {
console.error(error);
// some browsers will show an error dialogue for some
// errors; to be safe we always display an error message on the page.
window.GOVUK.ErrorBanner.showBanner();
});
});
};
};
function postWebAuthnCreateResponse(response, csrf_token) {
return fetch('/webauthn/register', {
method: 'POST',
headers: { 'X-CSRFToken': csrf_token },
body: window.CBOR.encode({
attestationObject: new Uint8Array(response.attestationObject),
clientDataJSON: new Uint8Array(response.clientDataJSON),
})
});
}
})(window);

View File

@@ -82,7 +82,7 @@
this.render();
}
this.$form.on('click', 'button.usa-button--event', (event) => this.actionButtonClicked(event));
this.$form.on('click', 'button.govuk-button--secondary', (event) => this.actionButtonClicked(event));
this.$form.on('change', 'input[type=checkbox]', () => this.templateFolderCheckboxChanged());
};
@@ -91,7 +91,7 @@
$.each(this.states.filter(state => 'description' in state), (idx, state) => {
id = `${state.key}__description`;
description = `<p class="usa-sr-only" id="${id}">${state.description}</p>`;
description = `<p class="govuk-visually-hidden" id="${id}">${state.description}</p>`;
state.$el
.prepend(description)
.attr('aria-describedby', id);
@@ -149,7 +149,7 @@
this.makeButton = (text, opts) => {
let $btn = $('<a href=""></a>')
.html(text)
.addClass('usa-link js-cancel')
.addClass('govuk-link govuk-link--no-visited-state js-cancel')
// isn't set if cancelSelector is undefined
.data('target', opts.cancelSelector || undefined)
.attr('tabindex', '0')
@@ -162,7 +162,7 @@
});
if (opts.hasOwnProperty('nonvisualText')) {
$btn.append(`<span class="usa-sr-only"> ${opts.nonvisualText}</span>`);
$btn.append(`<span class="govuk-visually-hidden"> ${opts.nonvisualText}</span>`);
}
return $btn;
@@ -285,6 +285,9 @@
if (['move-to-existing-folder', 'add-new-template'].indexOf(this.currentState) !== -1) {
mode = 'dialog';
}
GOVUK.stickAtBottomWhenScrolling.setMode(mode);
// make sticky JS recalculate its cache of the element's position
GOVUK.stickAtBottomWhenScrolling.recalculate();
if (currentStateObj && ('setFocus' in currentStateObj)) {
scrollTop = $(window).scrollTop();
@@ -296,12 +299,10 @@
this.nothingSelectedButtons = $(`
<div id="nothing_selected">
<div class="js-stick-at-bottom-when-scrolling">
<div class="usa-button-group">
<button class="usa-button usa-button--event" value="add-new-template" aria-expanded="false">
New template
</button>
<button class="usa-button usa-button--event" value="add-new-folder" aria-expanded="false">New folder</button>
</div>
<button class="govuk-button govuk-button--secondary govuk-!-margin-right-3 govuk-!-margin-bottom-1" value="add-new-template" aria-expanded="false">
New template
</button>
<button class="govuk-button govuk-button--secondary govuk-!-margin-bottom-1" value="add-new-folder" aria-expanded="false">New folder</button>
<div class="template-list-selected-counter">
<span class="template-list-selected-counter__count" aria-hidden="true">
${this.selectionStatus.default}
@@ -314,12 +315,10 @@
this.itemsSelectedButtons = $(`
<div id="items_selected">
<div class="js-stick-at-bottom-when-scrolling">
<div class="usa-button-group">
<button class="usa-button usa-button--event" value="move-to-existing-folder" aria-expanded="false">
Move<span class="usa-sr-only"> selection to folder</span>
</button>
<button class="usa-button usa-button--event" value="move-to-new-folder" aria-expanded="false">Add to new folder</button>
</div>
<button class="govuk-button govuk-button--secondary govuk-!-margin-right-3 govuk-!-margin-bottom-1" value="move-to-existing-folder" aria-expanded="false">
Move<span class="govuk-visually-hidden"> selection to folder</span>
</button>
<button class="govuk-button govuk-button--secondary govuk-!-margin-bottom-1" value="move-to-new-folder" aria-expanded="false">Add to new folder</button>
<div class="template-list-selected-counter" aria-hidden="true">
<span class="template-list-selected-counter__count" aria-hidden="true">
${this.selectionStatus.selected(1)}

View File

@@ -1,65 +0,0 @@
window.GOVUK = window.GOVUK || {};
window.GOVUK.Modules = window.GOVUK.Modules || {};
window.GOVUK.Modules.TimeoutPopup = window.GOVUK.Modules.TimeoutPopup || {};
(function(global) {
"use strict";
const sessionTimer = document.getElementById("sessionTimer");
let intervalId = null;
function checkTimer(timeTillSessionEnd) {
var now = new Date().getTime();
var difference = timeTillSessionEnd - now;
var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((difference % (1000 * 60)) / 1000);
document.getElementById("timeLeft").innerHTML = + minutes + "m " + seconds + "s";
showTimer();
document.getElementById("logOutTimer").addEventListener("click", signoutUser);
document.getElementById("extendSessionTimer").addEventListener("click", extendSession);
if (difference < 0) {
clearInterval(intervalId);
intervalId = null;
closeTimer();
expireUserSession();
}
}
function expireUserSession() {
var signOutLink = '/sign-out?next=' + window.location.pathname;
window.location.href = signOutLink;
}
function signoutUser() {
window.location.href = '/sign-out';
}
function extendSession() {
window.location.reload();
}
function showTimer() {
sessionTimer.showModal();
}
function closeTimer() {
sessionTimer.close();
}
function setSessionTimer() {
var timeTillSessionEnd = new Date().getTime() + (5 * 60 * 1000);
intervalId = setInterval(checkTimer, 1000, timeTillSessionEnd);
}
if (document.getElementById("timeLeft") !== null) {
setTimeout(setSessionTimer, 25 * 60 * 1000);
}
global.GOVUK.Modules.TimeoutPopup.checkTimer = checkTimer;
global.GOVUK.Modules.TimeoutPopup.expireUserSession = expireUserSession;
global.GOVUK.Modules.TimeoutPopup.signoutUser = signoutUser;
global.GOVUK.Modules.TimeoutPopup.extendSession = extendSession;
global.GOVUK.Modules.TimeoutPopup.showTimer = showTimer;
global.GOVUK.Modules.TimeoutPopup.closeTimer = closeTimer;
})(window);

View File

@@ -1,5 +0,0 @@
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-WX5NGWF');

View File

@@ -1,342 +0,0 @@
@use "uswds-core" as *;
// SMS Box Style
.sms-message-wrapper {
position: relative;
width: 100%;
max-width: 464px;
box-sizing: border-box;
padding: units(1);
background: color('gray-cool-10');
border: 1px solid color('gray-cool-10');
border-radius: 5px;
white-space: normal;
margin: 0 0 units(1) 0;
clear: both;
word-wrap: break-word;
&:after {
content: "";
display: block;
position: absolute;
bottom: -5px;
right: -20px;
border: 10px solid transparent;
border-left-width: 13px;
border-right-width: 13px;
border-bottom-color: color('gray-cool-10');
border-left-color: color('gray-cool-10');
transform: rotate(17deg);
}
}
.sms-message-inbound {
.sms-message-wrapper {
&:after {
border-left-color: transparent;
border-bottom-color: color('gray-cool-10');
border-right-color: color('gray-cool-10');
right: auto;
left: -20px;
transform: rotate(17deg);
}
}
}
.sms-message-sender {
margin: units(1) 0 0;
}
.sms-message-recipient {
color: color('gray-cool-90');
margin: 0 0 units(1);
}
.sms-message-status {
color: color('gray-cool-90');
margin: -20px units(1) 20px units(1);
}
.sms-message-status-outbound {
text-align: right;
}
.sms-message-row {
&:focus {
outline: none;
padding-top: 120px;
margin-top: -120px;
}
}
.sms-message-reply-link {
text-align: right;
}
// File upload area
.page-footer {
position: relative;
margin-bottom: 30px;
&-link {
line-height: 40px;
padding: 1px 0 0 15px;
font-weight: normal;
&:first-of-type {
padding-left: 0px;
}
}
&-delete-link-without-button {
padding: 0;
display: inline-block;
}
&-secondary-link {
display: block;
margin-top: units(1);
}
&-right-aligned-link {
position: absolute;
right: 0;
top: 9px; // align baseline with buttons
&-without-button {
position: absolute;
right: 0;
top: 0;
}
}
&__button {
margin-right: 10px;
}
.js-cancel {
margin: 0;
}
&__button--centred {
display: block;
margin: 0 auto;
}
}
.file-upload {
&-label,
&-button-label {
font-weight: bold;
font-size: 19px;
display: block;
margin: 0 0 10px 0;
}
&-label .error-message,
&-button-label.error-message {
padding: 0;
}
&-field {
margin-bottom: 10px;
}
// Hide normal upload form if we're adding a custom version with JS
.js-enabled &-label,
.js-enabled &-field,
.js-enabled &-submit {
display: none;
}
&-alternate-link {
display: inline-block;
line-height: 35px;
a {
font-weight: bold;
}
}
}
.spreadsheet {
margin-bottom: units (1);
.table {
margin-bottom: 0;
}
.usa-table--borderless thead th {
border-top: 1px solid color('gray-cool-10');
}
th,
.table-field-index {
background: color('gray-cool-10');
border: 1px solid color('gray-cool-10');
font-weight: bold;
text-align: center;
}
th, td {
padding-left: 10px;
padding-right: 10px;
border: 1px solid color('gray-cool-10');
}
td {
border-top: 0;
// 194 is the width of the table * 1/3.5, so the overflow cuts off
// at 3.5 columns wide.
// 11 accounts for the padding of the table cell
min-width: 194px - 11px;
&:first-child {
min-width: auto;
}
}
.fullscreen-fixed-table {
z-index: 1000;
.table-field-heading-first {
background: color('gray-cool-10');
}
}
}
.fullscreen {
&-content {
background: #FFFFFF;
z-index: 10;
overflow-y: hidden;
box-sizing: border-box;
margin: 0 0 units(1) 0;
padding: 0 0 0 0;
overflow: hidden;
border-bottom: 1px solid color('gray-cool-10');
.table {
margin-bottom: 0;
tr:last-child {
td {
border-bottom: 1px solid #FFFFFF;
}
}
}
th,
.table-field-error-label,
.table-field-left-aligned {
white-space: nowrap;
}
}
&-right-shadow {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
z-index: 200;
&.visible {
&.with-transition {
transition: box-shadow 0.6s ease-out;
}
box-shadow: inset -1px 0 0 0 color('gray-cool-10'), inset -3px 0 0 0 rgba(color('gray-cool-10'), 0.2);
}
}
&-scrollable-table {
overflow-x: auto;
overflow-y: hidden;
.table-field-heading-first,
.table-field-index {
display: none;
}
.table-field-left-aligned {
position: relative;
z-index: 150;
background: #FFFFFF;
}
&::-webkit-scrollbar {
-webkit-appearance: none;
}
&::-webkit-scrollbar:horizontal {
height: 11px;
background-color: #FFFFFF;
}
&::-webkit-scrollbar-thumb {
border-radius: 8px;
border: 2px solid #FFFFFF;
background-color: rgba(0, 0, 0, .5);
}
&::-webkit-scrollbar-track {
background-color: #FFFFFF;
border-radius: 8px;
}
}
&-fixed-table {
position: absolute;
top: 0;
overflow: hidden;
.table-field-heading {
visibility: hidden;
}
.table-field-left-aligned {
width: 0;
position: relative;
z-index: 100;
visibility: hidden;
}
.table-field-heading-first,
.table-field-index {
transition: none;
position: relative;
z-index: 200;
background: #FFFFFF;
}
}
&-scrolled-table {
padding-bottom: 20px;
.table-field-heading-first,
.table-field-index {
transition: box-shadow 0.3s ease-in-out;
box-shadow: 1px 0 0 0 color('gray-cool-10'), 3px 0 0 0 rgba(color('gray-cool-10'), 0.2);
}
}
&-shim {
width: 100%;
position: relative;
z-index: 9;
margin-bottom: 30px;
pointer-events: none;
& + .table-show-more-link {
margin-top: -20px;
}
}
}
.js-focus-style {
outline: 3px solid color("blue-40v");
box-shadow: 0 0 0 7px color("blue-40v");
*:focus {
outline: none;
}
}

View File

@@ -21,530 +21,3 @@ 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-nav__secondary {
.usa-nav__link {
padding: 0;
&.usa-current {
text-decoration: underline;
}
}
}
}
.usa-banner__header-action {
font-size: size("body", 2);
}
.usa-nav__primary {
&> .usa-nav__primary-item > a {
font-size: size("body", 4);
}
// &> .usa-nav__primary-item:last-child {
// margin-left: auto;
// @include u-margin-right(-4);
// }
}
h1 {
font-weight: bold !important;
}
p,
.list li {
font-family: family("sans");
font-size: size("body", 6);
}
.large-number {
font-size: size("body", 16);
}
.show-more-no-border:focus,
.show-more:focus,
.table-field-right-aligned a:active:before,
.table-field-right-aligned a:focus:before {
box-shadow: none !important;
}
.multiple-choice [type="radio"]:focus + label::before {
box-shadow: 0 0 0 3px color("blue-40v");
}
.usa-dark-background .pill-item__container {
color: #fff;
}
.pill-separate-item:link,
.pill-separate-item:visited {
text-decoration: none;
background: initial;
}
.template-footer .usa-link,
.template-content-count,
.table-empty-message,
td.table-empty-message {
font-size: size("body", "sm");
}
.textbox-highlight-background,
.textbox-highlight-foreground,
.textbox-highlight-mask,
.textbox-highlight-textbox {
font-size: units(2);
line-height: 1.2;
padding: units(1);
font-family: family("sans");
}
.usa-dark-background .pill-item__label {
color: white;
text-decoration: none !important;
}
.pill-item.usa-link {
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,
.template-list-item-without-ancestors .template-list-folder:focus::before,
.template-list-item-without-ancestors .template-list-template:active,
.template-list-item-without-ancestors .template-list-template:active::before,
.template-list-item-without-ancestors .template-list-template:focus,
.template-list-item-without-ancestors .template-list-template:focus::before {
background-color: transparent;
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;
background-position: 0;
}
.usa-checkbox .usa-checkbox__label {
+ a.template-list-folder {
margin-left: units(4);
}
}
.usa-checkbox__label-description {
margin: units(2px) 0 units(1) units(4);
}
.folder-heading-folder {
background: url(../img/material-icons/folder_open.svg) no-repeat;
padding-left: units(4);
display: inline-block;
background-size: units(3) units(3);
background-position: 0;
}
.message-name-separator:before, .folder-heading-separator:before {
content: ">";
margin: 0 units(0.5);
}
.template-list-template {
background: url(../img/material-icons/description.svg) no-repeat;
padding-left: units(4);
display: inline-flex;
}
.js-enabled .live-search {
display: flex;
margin-bottom: units(2);
.usa-form-group {
@include u-width("mobile-lg");
}
input#search {
width: 100%;
border: 1px solid color("gray-60");
}
}
.usa-hint {
display: block;
}
.js-stick-at-bottom-when-scrolling {
display: flex;
align-items: flex-end;
justify-content: space-between;
overflow: visible;
button {
margin-right: units(2);
}
.template-list-selected-counter {
position: relative;
top: 0;
}
}
.sticky-template-form {
margin: 0;
border: 1px solid color("gray-cool-10");
padding: units(2);
&:focus {
outline-offset: units(1);
}
}
.spark-bar-bar {
font-size: units(2);
background-color: transparent;
}
.usa-hero {
background-image: none;
}
.usa-prose {
h1, h2, h3, h4, h5 {
font-family: family("sans");
}
}
.usa-section {
&__home {
h2 {
font-family: family("serif");
}
}
}
.navigation-service.usa-breadcrumb {
bottom: 0;
}
// Dashboard
.dashboard {
.big-number-with-status {
a {
background: color("blue-60v");
padding: units(2);
margin-bottom: units(1);
text-decoration: none;
&:hover{
background: color("blue-warm-70v");
}
}
span {
color: white;
}
.big-number-smaller {
display: flex;
flex-direction: column;
.big-number-number {
font-size: units(5);
line-height: units(6);
}
.big-number-label {
font-size: units(3);
}
}
.big-number-status {
background: color("green-cool-40v");
display: flex;
padding: units(1) units(2);
&--failing {
padding: 0;
a.usa-link {
color: white;
background: color("red-warm-50v");
padding: units(1) units(2);
margin: 0;
width: 100%;
&:hover {
background: color("red-warm-60v");
}
}
}
}
}
.usa-table {
width: 100%;
caption {
margin-bottom: 0;
}
.table-field-center-aligned {
text-align: center;
}
.template-statistics-table-template-name {
padding-left: units(4);
display: inline-block;
background-repeat: no-repeat;
background-image: url(../img/material-icons/description.svg);
}
}
}
.dashboard-table {
table {
width: 100%;
}
.file-list-filename {
font-weight: bold;
}
.file-list-hint {
margin: 0;
}
.table-field, .table-field-right-aligned {
width: 50%;
}
&.usage-table {
.table-field, .table-field-left-aligned, .table-field-right-aligned {
width: auto;
}
}
}
.usage-table {
ul {
list-style: none;
padding: 0;
margin: 0;
}
.big-number-smallest {
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;
}
}
// Tabs
.tabs {
.pill {
display: flex;
list-style: none;
padding: 0;
.pill-item__container {
border: 1px solid color("gray-cool-10");
flex: 1;
display: flex;
flex-direction: column;
text-align: center;
font-size: units(2);
a {
padding: units(4);
.big-number-smaller {
font-size: units(5);
line-height: units(6);
}
.big-number-smallest {
font-size: units(3);
}
&:not(.pill-item--selected):hover {
background: color("blue-warm-70v");
}
&.pill-item--selected:hover {
color: color("blue-60v");
}
}
}
}
}
// Etc
.email-brand, .browse-list {
padding: 0;
margin: 0;
list-style: none;
margin-bottom: units(2);
li {
padding: 8px 0;
}
}
details form {
box-sizing:border-box;
}
// Textbox highlight
.textbox-highlight {
&-wrapper {
position: relative;
}
&-textbox {
resize: none;
z-index: 20;
background: none;
}
&-textbox,
&-background,
&-foreground,
&-mask {
font-size: units(2);
display: block;
box-sizing: border-box;
position: relative;
padding: 4px;
overflow: hidden;
line-height: 1.6;
}
&-background {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
color: transparent;
white-space: pre-wrap;
overflow-wrap: break-word;
word-wrap: break-word;
border: 2px solid transparent;
padding-bottom: units(3);
z-index: 10;
// transparent borders become visible in high contrast modes so set to match background
@media (-ms-high-contrast: active), (forced-colors: active) {
border-color: Canvas;
}
.placeholder,
.placeholder-conditional {
background-color: #fff;
position: relative;
&:after {
content: "";
background-color: color("yellow-20v");
border: 1px solid color("yellow-20v");
display: block;
position: absolute;
top: 0;
left: 5px;
right: 6px;
height: 100%;
border-radius: 7px;
}
}
}
}
.textbox-help-link {
margin: 10px 0 0 0;
}
.textbox-right-aligned {
text-align: right;
}
.extra-tracking .usa-input {
font-size: units(2);
padding-left: 5px;
letter-spacing: 0.04em;
}
.edit-textbox-error-mt {
margin-top: 1.5rem;
}

View File

@@ -7,7 +7,9 @@ in the form $setting: value,
----------------------------------------
*/
@use "uswds-core" with (
$theme-font-type-sans: "public-sans",
$theme-show-notifications: false
);
//
// @use "uswds-core" with (
// $setting: value,
// $setting: value
// );
//

View File

@@ -1,4 +1,3 @@
@forward "uswds-theme";
@forward "uswds";
@forward "uswds-theme-custom-styles";
@forward "legacy-styles";

View File

@@ -0,0 +1,115 @@
.govuk-grid-column-one-sixth {
@include govuk-grid-column(one-sixth, $class: false);
}
.govuk-grid-column-five-sixths {
@include govuk-grid-column(five-sixths, $class: false);
}
.govuk-grid-column-one-eighth {
@include govuk-grid-column(one-eighth, $class: false);
}
.govuk-grid-column-five-eighths {
@include govuk-grid-column(five-eighths, $class: false);
}
.govuk-grid-column-seven-eighths {
@include govuk-grid-column(seven-eighths, $class: false);
}
%top-gutter,
.top-gutter {
@extend %contain-floats;
display: block;
margin-top: govuk-spacing(6);
clear: both;
}
.top-gutter-4-3 {
@extend %top-gutter;
margin-top: govuk-spacing(7);
}
.top-gutter-1-2 {
@extend %top-gutter;
margin-top: govuk-spacing(3);
}
.top-gutter-1-3 {
@extend %top-gutter;
margin-top: govuk-spacing(2);
}
.top-gutter-2-3 {
@extend %top-gutter;
margin-top: govuk-spacing(4);
}
.top-gutter-0 {
margin-top: 0;
}
%bottom-gutter,
.bottom-gutter {
@extend %contain-floats;
margin-bottom: govuk-spacing(6);
clear: both;
}
.bottom-gutter-2-3 {
@extend %bottom-gutter;
margin-bottom: govuk-spacing(4);
}
.bottom-gutter-1-2 {
@extend %bottom-gutter;
margin-bottom: govuk-spacing(3);
}
.bottom-gutter-1-3 {
@extend %bottom-gutter;
margin-bottom: govuk-spacing(2);
}
.bottom-gutter-3-2 {
@extend %bottom-gutter;
margin-bottom: govuk-spacing(6) * 3/2;
}
.bottom-gutter-2 {
@extend %bottom-gutter;
margin-bottom: govuk-spacing(9);
}
.left-gutter {
padding-left: govuk-spacing(6);
}
.left-gutter-4-3 {
padding-left: govuk-spacing(7);
}
.align-with-heading {
display: block;
text-align: center;
margin-top: 13px;
padding-left: 2px;
padding-right: 2px;
}
.align-with-heading-copy {
display: block;
margin-top: 25px;
}
.align-with-heading-copy-right {
display: block;
margin: 21px 0 19px 0;
text-align: right;
}
.align-with-big-number-hint {
margin-top: govuk-spacing(8);
}

View File

@@ -0,0 +1,12 @@
// Copy of _url-helpers.scss in govuk_frontend_toolkit
// to prepend the path to where we store images
@function file-url($file) {
$url: '';
@if $path {
$url: url($path + $file);
} @else {
$url: url($file);
}
@return $url;
}

View File

@@ -0,0 +1,255 @@
// Extra CSS overlaying elements
.form-control-1-1 {
width: 100%;
}
.form-control-5em {
width: 100%;
@include media(tablet) {
width: 5em;
}
}
.column-main {
.heading-large,
> .heading-medium {
margin: govuk-spacing(3) 0 govuk-spacing(4) 0;
word-wrap: break-word;
&.top-gutter-0 {
margin-top: 0;
}
}
}
.highlight {
font-family: monospace;
overflow-x: scroll;
padding: 10px 0 10px 10px;
}
.inline {
.block-label {
@include media(tablet) {
float: none;
display: inline-block;
}
}
}
td {
vertical-align: top;
}
.heading-xlarge {
margin-bottom: 20px;
}
.heading-medium {
margin-top: govuk-spacing(6);
}
.form-label {
margin-bottom: 5px;
&.heading-small {
@include bold-19();
}
}
.hint {
color: $secondary-text-colour;
.form-label + & {
display: block;
margin-top: -5px;
margin-bottom: 5px;
}
}
.list-bullet {
.list-bullet {
list-style: circle;
}
}
details summary {
text-decoration: underline;
margin-bottom: govuk-spacing(3);
}
.spreadsheet {
margin-bottom: -1 * govuk-spacing(6);
.table {
margin-bottom: 0;
}
th,
.table-field-index {
background: $grey-4;
font-weight: bold;
text-align: center;
}
th, td {
padding-left: 10px;
padding-right: 10px;
border: 1px solid $border-colour;
}
td {
border-top: 0;
// 194 is the width of the table * 1/3.5, so the overflow cuts off
// at 3.5 columns wide.
// 11 accounts for the padding of the table cell
min-width: 194px - 11px;
&:first-child {
min-width: auto;
}
}
.fullscreen-fixed-table {
z-index: 1000;
.table-field-heading-first {
background: $grey-4;
}
}
}
.body-copy-table {
table {
th,
td {
@include core-19;
word-wrap: break-word;
}
thead {
th {
@include bold-19;
}
}
}
}
.tabular-numbers {
@include core-19($tabular-numbers: true);
}
summary::-moz-details-marker {
display: none;
}
summary::-ms-details-marker {
display: none;
}
summary::-o-details-marker {
display: none;
}
summary::details-marker {
display: none;
}
summary::-webkit-details-marker {
display: none;
}
details .arrow {
font-size: 16px;
}
.block-label-hint {
@include core-16;
margin-top: 5px;
}
.multiple-choice input:disabled+label {
opacity: 1;
color: $secondary-text-colour;
cursor: default;
}
.heading-inline {
display: inline-block;
}
.bordered-text-box {
padding: 5px;
outline: 2px solid $black;
max-width: 100%;
}
.nowrap {
white-space: nowrap;
}
.heading-upcoming-jobs {
margin-top: govuk-spacing(3);
}
@keyframes live-pulse {
0% {
background: $red;
box-shadow: inset 0 0 0 2px $red, inset 0 0 0 4px $white;
}
40% {
background: $red;
box-shadow: inset 0 0 0 2px $red, inset 0 0 0 4px $white;
}
50% {
background: $white;
box-shadow: inset 0 0 0 2px $red, inset 0 0 0 2px $white;
}
100% {
background: $white;
box-shadow: inset 0 0 0 2px $red, inset 0 0 0 4px $white;
}
}
input:-webkit-autofill::first-line {
@include core-19;
}
.pilot-disabled {
a {
background-color: gray;
border-color: gray;
span {
color: lightgray;
}
}
.big-number-status {
background-color: gray;
border-color: gray;
color: lightgray;
}
& > * {
color: gray;
}
}

View File

@@ -0,0 +1,189 @@
.area-list {
display: inline;
&-item {
display: inline-block;
border: 2px solid $black;
// Create space for the remove link on the right of the list item (including borders)
padding: (govuk-spacing(1) + 1px) (govuk-spacing(2) + 37px) govuk-spacing(1) govuk-spacing(2);
margin: 0 govuk-spacing(1) govuk-spacing(2) 0;
position: relative;
&-remove {
display: block; // fallback for browsers that don't support flexbox
display: flex;
// center SVG vertically and horizontally
align-items: center;
justify-content: center;
position: absolute;
top: -2px; // cover list-item border-top
right: -2px; // cover list-item border-right
bottom: -2px; // cover list-item border-bottom
width: 37px;
padding: 0;
border: 2px solid $govuk-blue;
border-left: none;
text-align: center;
font-size: inherit; // counter govuk-button styles
line-height: 36px; // match area-list-item for browsers that don't support flexbox
// set styles so they don't get overriden by govuk-button styles
&,
&:hover {
background: $govuk-blue;
}
&:hover {
color: $light-blue-25;
}
// set styles so they don't get overriden by govuk-button styles
&:active {
top: -2px;
}
// The box-shadow that separates the remove link from the list item is hidden in
// high contrast mode. Remake using a border instead
// Also sets text colour to match govuk-button
@media (-ms-high-contrast: active), (forced-colors: active) {
&:link,
&:active,
&:hover,
&:focus:not(:active):not(:hover) {
border-left: 2px solid $govuk-blue;
color: LinkText;
}
}
// set styles so they don't get overriden by govuk-button styles
&,
&:focus,
&:focus:not(:active):not(:hover) {
box-shadow: -2px 0 0 0 $black, inset 1px 0 0 0 rgba($white, 0.1);
}
// set styles so they don't get overriden by govuk-button styles
&:focus,
&:focus:not(:active):not(:hover) {
border-color: $black;
}
&:focus:hover {
background-color: $govuk-focus-colour;
color: $black;
}
// show outline to make focus visible when background colours are overridden
@media (-ms-high-contrast: active), (forced-colors: active) {
// set styles so they don't get overriden by govuk-button styles
&:focus:not(:active):not(:hover) {
outline: 3px solid transparent;
}
// override hover styles when focused, to preserve dimensions and outline
&:focus:hover {
top: -2px;
right: -2px;
bottom: -2px;
width: 37px;
}
}
}
&:last-child {
margin-right: govuk-spacing(1) * 1.5;
}
&--unremoveable {
padding-right: govuk-spacing(2);
background: $light-blue-25;
color: mix($black, $govuk-blue, 66%);
border-color: $light-blue-25;
margin: 0 govuk-spacing(1) govuk-spacing(2) 0;
font-weight: bold;
}
&--smaller {
@include bold-16;
}
}
.govuk-button--secondary {
margin-left: 3px;
}
}
#area-list-map {
z-index: 50;
margin-bottom: govuk-spacing(4);
}
.area-list-key {
display: inline-block;
position: relative;
margin-right: govuk-spacing(3);
margin-bottom: govuk-spacing(4);
height: govuk-spacing(7);
padding-left: govuk-spacing(8);
line-height: govuk-spacing(7);
&__icon {
position: absolute;
left: -5px;
top: -7px;
}
&--certain {
&:before {
border: 2px solid $black;
background: $light-blue-50;
}
}
&--likely {
&:before {
padding: 1px;
border: 2px dashed $govuk-blue;
border-image: file-url('dashed-border-govuk-blue.svg') 4 round;
background: $light-blue-25;
}
}
&--phone-estimate {
padding-left: govuk-spacing(1);
margin-right: 0;
float: right;
&:before {
display: none;
}
}
}
.area-list-map-details {
.govuk-details__summary {
display: block;
padding-top: govuk-spacing(1);
padding-bottom: govuk-spacing(1);
margin-top: 1px;
}
.govuk-details__text {
padding: 0;
border: none;
border-bottom: 1px solid $border-colour;
margin-top: -1px;
}
}

View File

@@ -0,0 +1,217 @@
%banner,
.banner,
.banner-default {
@include bold-19;
color: $button-colour;
display: block;
padding: govuk-spacing(3);
margin: govuk-spacing(3) 0 govuk-spacing(6) 0;
text-align: left;
position: relative;
clear: both;
border: 5px solid $button-colour;
&-title {
@include bold-24;
}
p {
margin: 10px 0 5px 0;
}
.list-bullet {
@include copy-19;
}
.page-footer {
margin-bottom: govuk-spacing(1);
}
}
%banner-with-tick,
.banner-with-tick {
padding: govuk-spacing(3) (govuk-spacing(6) + govuk-spacing(3));
background-image: file-url('tick.png');
background-size: 19px;
background-repeat: no-repeat;
background-position: govuk-spacing(3) govuk-spacing(3);
}
.banner-default-with-tick {
@extend %banner;
@extend %banner-with-tick;
}
.banner-dangerous {
@extend %banner;
@include bold-19;
background: $white;
color: $text-colour;
border: 5px solid $error-colour;
margin: 15px 0;
text-align: left;
&:focus {
outline: 3px solid $govuk-focus-colour;
}
.list {
margin-bottom: 0;
}
}
.banner-tour {
@extend %banner;
background: $govuk-blue;
color: $white;
margin-top: 10px;
margin-bottom: 0;
padding: govuk-spacing(6);
height: 425px;
overflow: hidden;
box-shadow: inset 0 -1em 1.6em 0 rgba(0, 0, 0, 0.05);
border: 0;
&-no-fixed-height {
height: auto;
padding-top: govuk-spacing(8);
min-height: 425px;
}
&-with-service-name {
margin-top: -10px;
padding-top: 0;
box-shadow: 0 -1px 0 0 darken($govuk-blue, 10%);
}
.heading-medium {
@include core-24;
}
p {
margin-top: 0;
margin-bottom: govuk-spacing(6);
color: $white;
&:last-child {
margin-bottom: 0;
}
& + p {
margin-top: -1 * govuk-spacing(3);
}
}
ul {
@include core-24;
color: $white;
margin-bottom: govuk-spacing(5);
}
li {
margin-bottom: govuk-spacing(3);
list-style-type: disc;
margin-left: govuk-spacing(6);
}
a {
font-weight: bold;
display: block;
padding: 0 ;
margin: 0 0 govuk-spacing(6) 0;
&:link,
&:visited {
color: $white;
}
&:hover {
color: $white;
background-color: $link-hover-colour;
box-shadow: 0 0 0 10px $link-hover-colour;
}
&:focus,
&:active {
color: $govuk-focus-text-colour;
background-color: $govuk-focus-colour;
box-shadow: 0 0 0 10px $govuk-focus-colour, 0 4px 0 10px $govuk-focus-text-colour;
}
}
img {
width: 100%;
height: auto;
position: relative;
top: -1 * govuk-spacing(2);
}
.greyed-out-step {
opacity: 0.6;
}
}
.banner-dashboard {
$baseline-shift: 5px;
display: block; // for browsers that don't support flexbox
display: flex;
align-items: baseline;
flex-wrap: wrap;
padding: (govuk-spacing(3) - 1px) 0 (govuk-spacing(3) + 1px) 0;
border-top: 1px solid $border-colour;
border-bottom: 1px solid $border-colour;
margin-bottom: govuk-spacing(6);
text-decoration: none;
&:focus {
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
}
&-count,
&-meta {
float: left; // for browsers that don't support flexbox
}
&-count {
@include govuk-font(36, $weight: bold, $tabular: true);
padding-right: 8px;
position: relative;
// remove the top of the extra line-height this introduces
top: $baseline-shift;
margin-top: -$baseline-shift;
flex: 0 1 0.85ch;
}
&-count-label {
@include govuk-font(24, $weight: bold);
text-decoration: underline;
padding-right: govuk-spacing(6);
margin: 10px 0px 5px; // 10px includes 5px extra to counter the -5px margin-top on the count item
flex: 2 1 auto;
}
&-meta {
@include govuk-font(19);
float: right;
text-align: right;
flex: initial;
}
& + .banner-dashboard {
margin-top: -1 * govuk-spacing(6);
border-top: none;
}
}

View File

@@ -0,0 +1,195 @@
%big-number,
.big-number {
display: block;
&-number,
&-label {
display: block;
}
&-number {
@include bold-48($tabular-numbers: true);
}
&-label {
@include core-19;
padding-bottom: 10px;
}
}
.pill .big-number-number {
// reduce padding until screen is bigger than 420px / zoomed below 300%
padding-left: govuk-spacing(1);
@include govuk-media-query($from: 420px) {
padding-left: govuk-spacing(2);
}
}
.big-number-dark {
@extend %big-number;
padding: govuk-spacing(3);
position: relative;
background: $black;
color: $white;
.big-number-number {
@include bold-36($tabular-numbers: true);
}
}
.big-number-smaller {
@extend %big-number;
.big-number-number {
@include bold-36($tabular-numbers: true);
}
}
.big-number-smallest {
@extend %big-number;
.big-number-number {
@include bold-24($tabular-numbers: true);
}
}
.big-number-with-status {
@extend %big-number;
position: relative;
margin-bottom: govuk-spacing(4);
.big-number,
.big-number-smaller {
padding: govuk-spacing(3);
position: relative;
background: $black;
color: $white;
}
.big-number-label {
padding-bottom: 0;
&:link,
&:visited {
color: $link-colour;
}
}
.big-number-link {
text-decoration: none;
background: $link-colour;
color: $white;
display: block;
border: 2px solid $link-colour;
margin-bottom: 5px;
&:hover {
color: $light-blue-25;
.big-number,
.big-number-number,
.big-number-smaller,
.big-number-label {
color: $light-blue-25;
}
}
&:active,
&:focus {
background: $govuk-focus-colour;
border: none;
padding: 2px; /* replace the spacing the border gave */
/* override the default focus style to inset the underline */
box-shadow: inset 0 -4px $govuk-focus-text-colour;
.big-number-number,
.big-number-smaller,
.big-number-label {
color: $govuk-focus-text-colour;
text-decoration: none;
}
}
.big-number,
.big-number-smaller {
background: transparent;
}
.big-number-label {
text-decoration: underline;
}
}
%big-number-status,
.big-number-status {
@include core-19;
display: block;
background: $green;
color: $white;
padding: 15px;
a {
&:link,
&:visited,
&:active,
&:hover {
color: $white;
text-decoration: underline;
}
&:active,
&:focus {
color: $black;
}
}
}
.big-number-status-failing {
@extend %big-number-status;
background: $error-colour;
}
}
.big-number-meta-wrapper {
position: relative;
margin: govuk-spacing(3) 0 govuk-spacing(6) 0;
background: $govuk-blue;
.big-number-meta {
padding: govuk-spacing(2) govuk-spacing(3);
color: $white;
pointer-events: none;
@include media(desktop) {
position: absolute;
bottom: 7px;
right: 5px;
text-align: right;
}
}
}

View File

@@ -0,0 +1,28 @@
.browse-list {
margin-bottom: govuk-spacing(3);
.browse-sub-list {
margin-top: govuk-spacing(3);
margin-left: govuk-spacing(6);
@include media('desktop') {
margin-left: govuk-spacing(9);
}
}
&-item,
&-sub-item {
@include bold-24;
list-style: none;
margin-bottom: govuk-spacing(3);
}
&-link {
@include govuk-font(24, $weight: bold);
}
&-hint {
@include core-19;
margin: 5px 0 10px 0;
color: $secondary-text-colour;
}
}

View File

@@ -0,0 +1,120 @@
// Taken from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/checkboxes/_checkboxes.scss
$govuk-touch-target-size: 44px;
$govuk-checkboxes-size: 40px;
.selection-summary {
.selection-summary__text {
@include core-19($tabular-numbers: true);
padding: 5px 0 0 0;
margin-bottom: govuk-spacing(3);
&:focus {
outline: none;
}
}
.selection-summary__text--folders {
padding: 10px 15px 5px 51px;
background-image: file-url('folder-black.svg');
background-repeat: no-repeat;
background-size: 39px auto;
background-position: 0px 4px;
}
}
.checkboxes-nested {
margin-bottom: 10px;
.multiple-choice {
$circle-diameter: 39px;
$border-thickness: 4px;
$border-indent: ($circle-diameter / 2) - ($border-thickness / 2);
$border-colour: $border-colour;
float: none;
position: relative;
&:before {
content: "";
position: absolute;
bottom: 0;
left: $border-indent;
width: $border-thickness;
height: 100%;
background: $border-colour;
}
label {
float: none;
}
[type=checkbox]+label::before {
// To overlap the grey inset line
background: $white;
}
ul {
// To equalise the spacing between the line and the top/bottom of
// the radio
margin-top: 5px;
margin-bottom: -5px;
padding-left: 12px;
}
}
}
.govuk-form-group--nested {
$border-thickness: $govuk-touch-target-size - $govuk-checkboxes-size;
$border-indent: $govuk-touch-target-size / 2;
position: relative;
// To equalise the spacing between the line and the top/bottom of
// the radio
margin-top: govuk-spacing(1) + ($border-thickness / 2);
margin-bottom: govuk-spacing(1) * -1;
padding-left: govuk-spacing(2) + 2;
&:before {
content: "";
position: absolute;
bottom: 0;
left: $border-indent * -1;
width: $border-thickness;
height: 100%;
background: $govuk-border-colour;
}
}
.selection-content {
margin-bottom: govuk-spacing(4);
.checkboxes-nested {
margin-bottom: 0;
}
}
.selection-footer {
clear: both;
}
.selection-footer__button {
// revert full-width button for smaller screens
display: inline-block;
width: auto;
}
// styles specific to the collapsible checkboxes module
.selection-wrapper {
fieldset:focus {
outline: none;
}
}

View File

@@ -0,0 +1,27 @@
$border-thickness: 4px;
$top-spacing: govuk-spacing(5);
.multiple-choice {
z-index: 10;
.block-label {
&:before {
box-shadow: 0 5px 0 0 $white;
}
}
}
.conditional-radios {
&-panel {
border-left: $border-thickness solid $border-colour;
margin: 0 0 (-1 * govuk-spacing(3)) (govuk-spacing(3) + ($border-thickness / 2));
padding: govuk-spacing(3) 0 0 (govuk-spacing(6) - ($border-thickness / 2));
position: relative;
top: -$top-spacing;
z-index: 1;
}
}

View File

@@ -0,0 +1,43 @@
.content-metadata {
@include govuk-font(16);
@include govuk-clearfix;
@include govuk-responsive-margin(5, "bottom");
color: $govuk-text-colour;
margin-top: 15px;
.content-metadata__term,
.content-metadata__definition {
line-height: 1.4;
}
.content-metadata__term {
margin-top: .5em;
@include govuk-media-query($from: tablet) {
box-sizing: border-box;
float: left;
clear: left;
padding-right: govuk-spacing(1);
margin-top: 0;
}
}
.content-metadata__definition {
margin: 0;
@include govuk-media-query($from: tablet) {
&:not(:last-of-type) {
margin-bottom: govuk-spacing(1);
}
}
}
.content-metadata__suffix {
margin-top: 0.628em;
margin-bottom: 0;
@include govuk-media-query($from: tablet) {
margin-top: 6px;
}
}
}

View File

@@ -0,0 +1,122 @@
// GOV.UK Publishing components cookie banner styles
// https://github.com/alphagov/govuk_publishing_components/blob/master/app/assets/stylesheets/govuk_publishing_components/components/_cookie-banner.scss
// component uses .govuk-body and .govuk-button classes from govuk-frontend
@import 'core/typography';
@import 'components/button/_button';
.notify-cookie-banner__wrapper {
@include govuk-responsive-padding(4, "top");
@include govuk-responsive-padding(5, "bottom");
}
// component should only be shown if JS is available, by the cookieMessage JS, so hide by default
.notify-cookie-banner {
display: none;
}
.notify-cookie-banner__buttons {
display: flex;
flex-wrap: wrap;
@include govuk-media-query($from: tablet) {
flex-wrap: nowrap;
}
}
.notify-cookie-banner__button,
.notify-cookie-banner__link {
vertical-align: baseline;
}
.notify-cookie-banner__button {
display: inline-block;
flex: 1 0;
padding-left: govuk-spacing(9);
padding-right: govuk-spacing(9);
margin-bottom: govuk-spacing(2);
@include govuk-media-query($from: tablet) {
flex: 0 0 150px;
padding-left: govuk-spacing(2);
padding-right: govuk-spacing(2);
margin-bottom: govuk-spacing(1);
}
}
.notify-cookie-banner__button-accept {
margin-right: govuk-spacing(4);
}
.notify-cookie-banner__link {
@include govuk-font(19);
line-height: 1;
display: block;
width: 100%;
padding: 9px 0px 6px;
@include govuk-media-query($from: tablet) {
display: inline;
width: auto;
margin-left: govuk-spacing(6);
}
}
.notify-cookie-banner__confirmation {
display: none;
position: relative;
padding: govuk-spacing(4) 0;
@include govuk-media-query($from: desktop) {
padding: govuk-spacing(4);
}
// This element is focused using JavaScript so that it's being read out by screen readers
// for this reason we don't want to show the default outline or emphasise it visually using `govuk-focused-text`
&:focus {
outline: none;
}
}
.notify-cookie-banner__confirmation-message,
.notify-cookie-banner__hide-button {
display: block;
@include govuk-media-query($from: desktop) {
display: inline-block;
}
}
.notify-cookie-banner__confirmation-message {
margin-right: govuk-spacing(4);
@include govuk-media-query($from: desktop) {
max-width: 90%;
}
}
.notify-cookie-banner__hide-button {
@include govuk-font($size: 19);
color: $govuk-link-colour;
outline: 0;
border: 0;
background: none;
text-decoration: underline;
padding: govuk-spacing(0);
margin-top: govuk-spacing(2);
right: govuk-spacing(3);
cursor: pointer;
@include govuk-media-query($from: desktop) {
margin-top: govuk-spacing(0);
position: absolute;
right: govuk-spacing(4);
}
}
// Additions
// Override margin-bottom, inherited from using .govuk-body class
.notify-cookie-banner__confirmation-message {
margin-bottom: 0;
}

View File

@@ -0,0 +1,33 @@
.copy-to-clipboard {
position: relative;
padding-bottom: 38px; // height of button
display: flex;
&__name {
@include bold-19;
margin-bottom: 5px;
}
&__notice,
&__value {
font-family: monospace;
display: block;
padding: 0 0 10px 0;
margin: auto 0;
}
&__button--show,
&__button--copy {
position: absolute;
bottom: 2px;
left: 0px;
&:active {
top: auto;
}
}
}

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