Compare commits
14 Commits
01-30-2024
...
notify-473
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeee69cf43 | ||
|
|
d3905c22ce | ||
|
|
dc4df683f9 | ||
|
|
5c56404aac | ||
|
|
125dd56921 | ||
|
|
2c4e119a6b | ||
|
|
15f3f261fb | ||
|
|
a3b18070ae | ||
|
|
3ab979347c | ||
|
|
6015a05d9f | ||
|
|
115953dbcf | ||
|
|
63b16e0ae7 | ||
|
|
647fe4f40b | ||
|
|
a91ba71085 |
71
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: "Bug Report"
|
||||
description: "Use this template to file a bug report"
|
||||
title: "Bug: [Brief description of the bug]"
|
||||
|
||||
labels:
|
||||
- "Type: Bug"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please use this form to file new bugs! Add screenshots to the comments of this initial form.
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Who discovered this?
|
||||
description: Was it something you found or are you filing for someone else? If someone else, how can we contact them?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: In which environment did you experience the bug?
|
||||
options:
|
||||
- staging
|
||||
- demo
|
||||
- production
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: page
|
||||
attributes:
|
||||
label: What pages did this appear on?
|
||||
description: please include URLs of where you saw the bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproSteps
|
||||
attributes:
|
||||
label: "Detail the steps for someone to reproduce"
|
||||
description: "Reproduction steps"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
76
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
@@ -1,76 +0,0 @@
|
||||
name: "User Story Template"
|
||||
description: "Use this template for creating user stories"
|
||||
title: "User Story: [Brief description of the user story]"
|
||||
|
||||
labels:
|
||||
- "Type: User Story"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**User Story:**'
|
||||
- type: textarea
|
||||
id: userType
|
||||
attributes:
|
||||
label: "As a [type of user],"
|
||||
description: "Describe the type of user involved in this story."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actionFeature
|
||||
attributes:
|
||||
label: "I want [an action or feature],"
|
||||
description: "Describe the desired action or feature from the user's perspective."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: benefitValue
|
||||
attributes:
|
||||
label: "So that [benefit or value]."
|
||||
description: "Describe the benefit or value the user expects from the action or feature."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Acceptance Criteria:**'
|
||||
- type: textarea
|
||||
id: acceptanceCriteria
|
||||
attributes:
|
||||
label: "Detailed condition or criteria that must be met for the user story to be considered complete."
|
||||
description: "List the acceptance criteria for the user story."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Tasks:**'
|
||||
- type: textarea
|
||||
id: tasks
|
||||
attributes:
|
||||
label: "List of specific tasks or sub-tasks that need to be done to implement the user story."
|
||||
description: "Outline the tasks necessary to implement the user story."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Dependencies:**'
|
||||
- type: textarea
|
||||
id: dependencies
|
||||
attributes:
|
||||
label: "List any dependencies that need to be resolved before starting or completing this user story."
|
||||
description: "Specify any dependencies related to the user story."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Notes:**'
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: "Any additional information or context that might be useful for the team."
|
||||
description: "Provide any extra notes or context for the user story."
|
||||
validations:
|
||||
required: false
|
||||
82
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: "QA Checklist"
|
||||
description: "Use this template to manually QA for prod push"
|
||||
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: Logging_in
|
||||
attributes:
|
||||
label: Log In
|
||||
options:
|
||||
- label: notify-staging.app.cloud.gov resolves
|
||||
- label: Clicking sign in takes you to login page
|
||||
- label: 2FA code is sent promptly
|
||||
- label: 2FA code input screen appears as expected
|
||||
- label: After code is entered, you are brought to the “choose service” screen
|
||||
- label: Choosing a service brings you to the dashboard for that service
|
||||
|
||||
- type: checkboxes
|
||||
id: send_template
|
||||
attributes:
|
||||
label: Create a template and send
|
||||
options:
|
||||
- label: Clicking “Send messages” brings you to templates page
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Uploading a csv is quick and automatically directs you to the preview/send page
|
||||
- label: Formatting looks clean and in place
|
||||
- label: Message dashboard populates and shows “pending” until call is made to AWS for delivery receipt
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__copied_template
|
||||
attributes:
|
||||
label: Send a message from copied template
|
||||
options:
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Clicking “copy an existing template” allows you to choose from pre-existing template
|
||||
- label: After editing, the new template saves and you prepare to send
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__existing_template
|
||||
attributes:
|
||||
label: Send a message from existing template
|
||||
options:
|
||||
- label: Clicking on existing template brings you to preview screen with `prepare to send` button
|
||||
- label: All else above persists as true (formatting, send, dashboard, etc)
|
||||
|
||||
- type: checkboxes
|
||||
id: download_report
|
||||
attributes:
|
||||
label: Download the csv delivery report
|
||||
options:
|
||||
- label: Clicking on the csv download directly after a message send downloads a report that contains the data we expect
|
||||
- label: The batch report appears on the main dashboard batch table with accurate information
|
||||
- label: Clicking on the csv download link from the main dashboard downloads a report that contains the data we expect
|
||||
|
||||
- type: checkboxes
|
||||
id: invite_member
|
||||
attributes:
|
||||
label: Invite a team member
|
||||
options:
|
||||
- label: Clicking on “Team” in settings brings you to team member screen
|
||||
- label: Clicking “Invite a team member” brings you to email and permissions screen
|
||||
- label: Clicking invite team member sends an invite and shows confirmation
|
||||
- label: Invite email provides live link that works to register new team member
|
||||
|
||||
- type: checkboxes
|
||||
id: delete_member
|
||||
attributes:
|
||||
label: Delete a team member
|
||||
options:
|
||||
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that person’s profile
|
||||
- label: Clicking on “Remove this team member” prompts a confirmation
|
||||
- label: Clicking the confirmation removes the team member
|
||||
|
||||
- type: checkboxes
|
||||
id: sign_out
|
||||
attributes:
|
||||
label: Sign Out
|
||||
options:
|
||||
- label: Hitting “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
4
.github/actions/setup-project/action.yml
vendored
@@ -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
|
||||
|
||||
16
.github/dependabot.yml
vendored
@@ -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'
|
||||
|
||||
49
.github/workflows/checks.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/daily_checks.yml
vendored
@@ -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
|
||||
|
||||
19
.github/workflows/deploy-demo.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
with:
|
||||
files: |
|
||||
terraform/demo
|
||||
@@ -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
|
||||
|
||||
87
.github/workflows/deploy-prod.yml
vendored
@@ -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
|
||||
35
.github/workflows/deploy.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
with:
|
||||
files: |
|
||||
terraform/staging
|
||||
@@ -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
|
||||
|
||||
36
.github/workflows/drift.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/terraform-production.yml
vendored
@@ -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
@@ -123,12 +123,3 @@ app/templates/vendor
|
||||
secrets.auto.tfvars
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
|
||||
# Playwright
|
||||
playwright/
|
||||
|
||||
# Pyenv
|
||||
.python-version
|
||||
|
||||
# Nodenv
|
||||
.node-version
|
||||
|
||||
4
.pa11yci
@@ -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": [
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: debug-statements
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
3
.profile
@@ -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
@@ -0,0 +1 @@
|
||||
3.9
|
||||
69
Makefile
@@ -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
@@ -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
478
README.md
@@ -1,8 +1,6 @@
|
||||
# Notify.gov Admin UI
|
||||
# Notify UI
|
||||
|
||||
This is the Notify front-end for government users and admins. To see it in
|
||||
action, check out [the demo site](https://notify-demo.app.cloud.gov)
|
||||
(contact team for credentials).
|
||||
This is the Notify front-end for government users and admins. To see it in action, check out [the demo site](https://notify-demo.app.cloud.gov) (contact team for credentials).
|
||||
|
||||
Through the interface, users can:
|
||||
|
||||
@@ -11,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
|
||||
[GSA’s Technology Transformation Services](https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services)
|
||||
(TTS), home to innovative programs like [18F](https://18f.gsa.gov/) and
|
||||
[Login.gov](https://login.gov). We collaborate with benefits programs to develop
|
||||
shared technology tools and best practices that reduce the burden of navigating
|
||||
government programs for low income individuals and families.
|
||||
The Public Benefits Studio is a team inside of [GSA’s Technology Transformation Services](https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services) (TTS), home to innovative programs like [18F](https://18f.gsa.gov/) and [Login.gov](https://login.gov). We collaborate with benefits programs to develop shared technology tools and best practices that reduce the burden of navigating government programs for low income individuals and families.
|
||||
|
||||
We’re a cross-functional team of technologists with specialized experience
|
||||
working across public benefits programs like Medicaid, SNAP, and unemployment
|
||||
insurance.
|
||||
We’re a cross-functional team of technologists with specialized experience working across public benefits programs like Medicaid, SNAP, and unemployment insurance.
|
||||
|
||||
For more information on what we're working on, the Notify tool, and how to get
|
||||
involved with our team,
|
||||
[see our flyer.](https://github.com/GSA/notifications-admin/blob/main/docs/notify-pilot-flyer.md)
|
||||
For more information on what we're working on, the Notify tool, and how to get involved with our team, [see our flyer.](https://github.com/GSA/notifications-admin/blob/main/docs/notify-pilot-flyer.md)
|
||||
|
||||
312
app/__init__.py
@@ -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("There’s something wrong with the link you’ve used.")
|
||||
flash('There’s something wrong with the link you’ve 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
app/assets/fonts/bold-a2452cb66f-v1.woff2
Normal file
BIN
app/assets/fonts/bold-f38c792ac2-v1.woff
Normal file
BIN
app/assets/fonts/bold-fb2676462a-v1.eot
Normal file
BIN
app/assets/fonts/bold-tabular-357fdfbcc3-v1.eot
Normal file
BIN
app/assets/fonts/bold-tabular-784c21afb8-v1.woff
Normal file
BIN
app/assets/fonts/bold-tabular-b89238d840-v1.woff2
Normal file
BIN
app/assets/fonts/light-2c037cf7e1-v1.eot
Normal file
BIN
app/assets/fonts/light-458f8ea81c-v1.woff
Normal file
BIN
app/assets/fonts/light-f38ad40456-v1.woff2
Normal file
BIN
app/assets/fonts/light-tabular-498ea8ffe2-v1.eot
Normal file
BIN
app/assets/fonts/light-tabular-62cc6f0a28-v1.woff
Normal file
BIN
app/assets/fonts/light-tabular-851b10ccdd-v1.woff2
Normal file
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 283 B |
|
Before Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -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"}
|
||||
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 15 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
69
app/assets/javascripts/analytics/analytics.js
Normal 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);
|
||||
40
app/assets/javascripts/analytics/init.js
Normal 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);
|
||||
74
app/assets/javascripts/authenticateSecurityKey.js
Normal 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);
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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>')
|
||||
);
|
||||
|
||||
|
||||
15
app/assets/javascripts/consent.js
Normal file
@@ -0,0 +1,15 @@
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
function hasConsentFor (cookieCategory, consentCookie) {
|
||||
if (consentCookie === undefined) { consentCookie = window.GOVUK.getConsentCookie(); }
|
||||
|
||||
if (consentCookie === null) { return false; }
|
||||
|
||||
if (!(cookieCategory in consentCookie)) { return false; }
|
||||
|
||||
return consentCookie[cookieCategory];
|
||||
}
|
||||
|
||||
window.GOVUK.hasConsentFor = hasConsentFor;
|
||||
})(window);
|
||||
104
app/assets/javascripts/cookieMessage.js
Normal file
@@ -0,0 +1,104 @@
|
||||
window.GOVUK = window.GOVUK || {};
|
||||
window.GOVUK.Modules = window.GOVUK.Modules || {};
|
||||
|
||||
(function (Modules) {
|
||||
function CookieBanner () { }
|
||||
|
||||
CookieBanner.clearOldCookies = function (consent) {
|
||||
var gaCookies = ['_ga', '_gid'];
|
||||
|
||||
// clear old cookie set by our previous JS, set on the www domain
|
||||
if (window.GOVUK.cookie('seen_cookie_message')) {
|
||||
document.cookie = 'seen_cookie_message=;expires=' + new Date().toGMTString() + ';path=/';
|
||||
}
|
||||
|
||||
if (consent === null) {
|
||||
for (var i = 0; i < gaCookies.length; i++) {
|
||||
if (window.GOVUK.cookie(gaCookies[i])) {
|
||||
// GA cookies are set on the base domain so need the www stripping
|
||||
var cookieString = gaCookies[i] + '=;expires=' + new Date().toGMTString() + ';domain=' + window.location.hostname.replace(/^www\./, '.') + ';path=/';
|
||||
document.cookie = cookieString;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.start = function ($module) {
|
||||
this.$module = $module[0];
|
||||
this.$module.hideCookieMessage = this.hideCookieMessage.bind(this);
|
||||
this.$module.showConfirmationMessage = this.showConfirmationMessage.bind(this);
|
||||
this.$module.setCookieConsent = this.setCookieConsent.bind(this);
|
||||
|
||||
this.$module.cookieBanner = document.querySelector('.notify-cookie-banner');
|
||||
this.$module.cookieBannerConfirmationMessage = this.$module.querySelector('.notify-cookie-banner__confirmation');
|
||||
|
||||
this.setupCookieMessage();
|
||||
};
|
||||
|
||||
CookieBanner.prototype.setupCookieMessage = function () {
|
||||
this.$hideLink = this.$module.querySelector('button[data-hide-cookie-banner]');
|
||||
if (this.$hideLink) {
|
||||
this.$hideLink.addEventListener('click', this.$module.hideCookieMessage);
|
||||
}
|
||||
|
||||
this.$acceptCookiesLink = this.$module.querySelector('button[data-accept-cookies=true]');
|
||||
if (this.$acceptCookiesLink) {
|
||||
this.$acceptCookiesLink.addEventListener('click', () => this.$module.setCookieConsent(true));
|
||||
}
|
||||
|
||||
this.$rejectCookiesLink = this.$module.querySelector('button[data-accept-cookies=false]');
|
||||
if (this.$rejectCookiesLink) {
|
||||
this.$rejectCookiesLink.addEventListener('click', () => this.$module.setCookieConsent(false));
|
||||
}
|
||||
|
||||
this.showCookieMessage();
|
||||
};
|
||||
|
||||
CookieBanner.prototype.showCookieMessage = function () {
|
||||
// Show the cookie banner if not in the cookie settings page
|
||||
if (!this.isInCookiesPage()) {
|
||||
var hasCookiesPolicy = window.GOVUK.cookie('cookies_policy');
|
||||
|
||||
if (this.$module && !hasCookiesPolicy) {
|
||||
this.$module.style.display = 'block';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.hideCookieMessage = function (event) {
|
||||
if (this.$module) {
|
||||
this.$module.style.display = 'none';
|
||||
}
|
||||
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.setCookieConsent = function (analyticsConsent) {
|
||||
window.GOVUK.setConsentCookie({ 'analytics': analyticsConsent });
|
||||
|
||||
this.$module.showConfirmationMessage(analyticsConsent);
|
||||
this.$module.cookieBannerConfirmationMessage.focus();
|
||||
|
||||
if (analyticsConsent) { window.GOVUK.initAnalytics(); }
|
||||
};
|
||||
|
||||
CookieBanner.prototype.showConfirmationMessage = function (analyticsConsent) {
|
||||
var messagePrefix = analyticsConsent ? 'You’ve accepted analytics cookies.' : 'You told us not to use analytics cookies.';
|
||||
|
||||
this.$cookieBannerMainContent = document.querySelector('.notify-cookie-banner__wrapper');
|
||||
this.$cookieBannerConfirmationMessage = document.querySelector('.notify-cookie-banner__confirmation-message');
|
||||
|
||||
this.$cookieBannerConfirmationMessage.insertAdjacentText('afterbegin', messagePrefix);
|
||||
this.$cookieBannerMainContent.style.display = 'none';
|
||||
this.$module.cookieBannerConfirmationMessage.style.display = 'block';
|
||||
};
|
||||
|
||||
CookieBanner.prototype.isInCookiesPage = function () {
|
||||
return window.location.pathname === '/cookies';
|
||||
};
|
||||
|
||||
Modules.CookieBanner = CookieBanner;
|
||||
})(window.GOVUK.Modules);
|
||||
|
||||
84
app/assets/javascripts/cookieSettings.js
Normal 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
163
app/assets/javascripts/govuk/cookie-functions.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// used by the cookie banner component
|
||||
|
||||
(function (root) {
|
||||
'use strict';
|
||||
window.GOVUK = window.GOVUK || {};
|
||||
|
||||
var DEFAULT_COOKIE_CONSENT = {
|
||||
'analytics': false
|
||||
};
|
||||
|
||||
var COOKIE_CATEGORIES = {
|
||||
'_ga': 'analytics',
|
||||
'_gid': 'analytics'
|
||||
};
|
||||
|
||||
/*
|
||||
Cookie methods
|
||||
==============
|
||||
|
||||
Usage:
|
||||
|
||||
Setting a cookie:
|
||||
GOVUK.cookie('hobnob', 'tasty', { days: 30 });
|
||||
|
||||
Reading a cookie:
|
||||
GOVUK.cookie('hobnob');
|
||||
|
||||
Deleting a cookie:
|
||||
GOVUK.cookie('hobnob', null);
|
||||
*/
|
||||
window.GOVUK.cookie = function (name, value, options) {
|
||||
if (typeof value !== 'undefined') {
|
||||
if (value === false || value === null) {
|
||||
return window.GOVUK.setCookie(name, '', { days: -1 });
|
||||
} else {
|
||||
// Default expiry date of 30 days
|
||||
if (typeof options === 'undefined') {
|
||||
options = { days: 30 };
|
||||
}
|
||||
return window.GOVUK.setCookie(name, value, options);
|
||||
}
|
||||
} else {
|
||||
return window.GOVUK.getCookie(name);
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.getConsentCookie = function () {
|
||||
var consentCookie = window.GOVUK.cookie('cookies_policy');
|
||||
var consentCookieObj;
|
||||
|
||||
if (consentCookie) {
|
||||
try {
|
||||
consentCookieObj = JSON.parse(consentCookie);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof consentCookieObj !== 'object' && consentCookieObj !== null) {
|
||||
consentCookieObj = JSON.parse(consentCookieObj);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return consentCookieObj;
|
||||
};
|
||||
|
||||
window.GOVUK.setConsentCookie = function (options) {
|
||||
var cookieConsent = window.GOVUK.getConsentCookie();
|
||||
|
||||
if (!cookieConsent) {
|
||||
cookieConsent = JSON.parse(JSON.stringify(DEFAULT_COOKIE_CONSENT));
|
||||
}
|
||||
|
||||
for (var cookieType in options) {
|
||||
cookieConsent[cookieType] = options[cookieType];
|
||||
|
||||
// Delete cookies of that type if consent being set to false
|
||||
if (!options[cookieType]) {
|
||||
for (var cookie in COOKIE_CATEGORIES) {
|
||||
if (COOKIE_CATEGORIES[cookie] === cookieType) {
|
||||
window.GOVUK.cookie(cookie, null);
|
||||
|
||||
if (window.GOVUK.cookie(cookie)) {
|
||||
document.cookie = cookie + '=;expires=' + new Date() + ';domain=' + window.location.hostname.replace(/^www\./, '.') + ';path=/';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.GOVUK.setCookie('cookies_policy', JSON.stringify(cookieConsent), { days: 365 });
|
||||
};
|
||||
|
||||
window.GOVUK.checkConsentCookieCategory = function (cookieName, cookieCategory) {
|
||||
var currentConsentCookie = window.GOVUK.getConsentCookie();
|
||||
|
||||
// If the consent cookie doesn't exist, but the cookie is in our known list, return true
|
||||
if (!currentConsentCookie && COOKIE_CATEGORIES[cookieName]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentConsentCookie = window.GOVUK.getConsentCookie();
|
||||
|
||||
// Sometimes currentConsentCookie is malformed in some of the tests, so we need to handle these
|
||||
try {
|
||||
return currentConsentCookie[cookieCategory];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.checkConsentCookie = function (cookieName, cookieValue) {
|
||||
// If we're setting the consent cookie OR deleting a cookie, allow by default
|
||||
if (cookieName === 'cookies_policy' || (cookieValue === null || cookieValue === false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (COOKIE_CATEGORIES[cookieName]) {
|
||||
var cookieCategory = COOKIE_CATEGORIES[cookieName];
|
||||
|
||||
return window.GOVUK.checkConsentCookieCategory(cookieName, cookieCategory);
|
||||
} else {
|
||||
// Deny the cookie if it is not known to us
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.setCookie = function (name, value, options) {
|
||||
if (window.GOVUK.checkConsentCookie(name, value)) {
|
||||
if (typeof options === 'undefined') {
|
||||
options = {};
|
||||
}
|
||||
var cookieString = name + '=' + value + '; path=/';
|
||||
if (options.days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (options.days * 24 * 60 * 60 * 1000));
|
||||
cookieString = cookieString + '; expires=' + date.toGMTString();
|
||||
}
|
||||
if (document.location.protocol === 'https:') {
|
||||
cookieString = cookieString + '; Secure';
|
||||
}
|
||||
document.cookie = cookieString;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.getCookie = function (name) {
|
||||
var nameEQ = name + '=';
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0, len = cookies.length; i < len; i++) {
|
||||
var cookie = cookies[i];
|
||||
while (cookie.charAt(0) === ' ') {
|
||||
cookie = cookie.substring(1, cookie.length);
|
||||
}
|
||||
if (cookie.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(cookie.substring(nameEQ.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}(window));
|
||||
|
||||
@@ -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>'
|
||||
|
||||
@@ -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');
|
||||
|
||||
36
app/assets/javascripts/previewPane.js
Normal 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);
|
||||
@@ -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>
|
||||
`)
|
||||
};
|
||||
|
||||
58
app/assets/javascripts/registerSecurityKey.js
Normal 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);
|
||||
@@ -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)}
|
||||
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// );
|
||||
//
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@forward "uswds-theme";
|
||||
@forward "uswds";
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@forward "legacy-styles";
|
||||
115
app/assets/stylesheets/_grids.scss
Normal 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);
|
||||
}
|
||||
12
app/assets/stylesheets/_url-helpers.scss
Normal 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;
|
||||
}
|
||||
255
app/assets/stylesheets/app.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
189
app/assets/stylesheets/components/area-list.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
217
app/assets/stylesheets/components/banner.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
195
app/assets/stylesheets/components/big-number.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
app/assets/stylesheets/components/browse-list.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
120
app/assets/stylesheets/components/checkboxes.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
27
app/assets/stylesheets/components/conditional-radios.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
43
app/assets/stylesheets/components/content-metadata.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
app/assets/stylesheets/components/cookie-message.scss
Normal 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;
|
||||
}
|
||||
33
app/assets/stylesheets/components/copy-to-clipboard.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||