Compare commits

..

1 Commits

Author SHA1 Message Date
Carlo Costino
57e9fc9ba1 Localize notification_utils to the Admin!
This changeset copies everything in notifications_utils to turn into a local project module.  It includes dependency changes to account for this adjustment and the notifications_utils tests.  It also fixes all of the import statements.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
2024-04-05 12:51:31 -04:00
659 changed files with 29379 additions and 29862 deletions

View File

@@ -1,638 +0,0 @@
{
"version": "1.5.0",
"plugins_used": [
{
"name": "ArtifactoryDetector"
},
{
"name": "AWSKeyDetector"
},
{
"name": "AzureStorageKeyDetector"
},
{
"name": "Base64HighEntropyString",
"limit": 4.5
},
{
"name": "BasicAuthDetector"
},
{
"name": "CloudantDetector"
},
{
"name": "DiscordBotTokenDetector"
},
{
"name": "GitHubTokenDetector"
},
{
"name": "GitLabTokenDetector"
},
{
"name": "HexHighEntropyString",
"limit": 3.0
},
{
"name": "IbmCloudIamDetector"
},
{
"name": "IbmCosHmacDetector"
},
{
"name": "IPPublicDetector"
},
{
"name": "JwtTokenDetector"
},
{
"name": "KeywordDetector",
"keyword_exclude": ""
},
{
"name": "MailchimpDetector"
},
{
"name": "NpmDetector"
},
{
"name": "OpenAIDetector"
},
{
"name": "PrivateKeyDetector"
},
{
"name": "PypiTokenDetector"
},
{
"name": "SendGridDetector"
},
{
"name": "SlackDetector"
},
{
"name": "SoftlayerDetector"
},
{
"name": "SquareOAuthDetector"
},
{
"name": "StripeDetector"
},
{
"name": "TelegramBotTokenDetector"
},
{
"name": "TwilioKeyDetector"
}
],
"filters_used": [
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": ".ds.baseline"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
},
{
"path": "detect_secrets.filters.heuristic.is_indirect_reference"
},
{
"path": "detect_secrets.filters.heuristic.is_likely_id_string"
},
{
"path": "detect_secrets.filters.heuristic.is_lock_file"
},
{
"path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
},
{
"path": "detect_secrets.filters.heuristic.is_potential_uuid"
},
{
"path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
},
{
"path": "detect_secrets.filters.heuristic.is_sequential_string"
},
{
"path": "detect_secrets.filters.heuristic.is_swagger_file"
},
{
"path": "detect_secrets.filters.heuristic.is_templated_secret"
}
],
"results": {
".github/workflows/checks.yml": [
{
"type": "Secret Keyword",
"filename": ".github/workflows/checks.yml",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 68,
"is_secret": false
}
],
"app/assets/js/uswds.min.js": [
{
"type": "Secret Keyword",
"filename": "app/assets/js/uswds.min.js",
"hashed_secret": "372ea08cab33e71c02c651dbc83a474d32c676ea",
"is_verified": false,
"line_number": 85,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "app/assets/js/uswds.min.js",
"hashed_secret": "53e07a32bf191d6917ee6fd863f0b52632a86798",
"is_verified": false,
"line_number": 85,
"is_secret": false
}
],
"app/config.py": [
{
"type": "Secret Keyword",
"filename": "app/config.py",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false,
"line_number": 122,
"is_secret": false
}
],
"app/main/_commonly_used_passwords.py": [
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "82e19fa12aab7cfc718a002fc82c0f074bf070e7",
"is_verified": false,
"line_number": 123,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "a172ffc990129fe6f68b50f6037c54a1894ee3fd",
"is_verified": false,
"line_number": 240,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "4de69ee6b12b7fc91070873b71ba6e2929b90619",
"is_verified": false,
"line_number": 244,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "370194ff6e0f93a7432e16cc9badd9427e8b4e13",
"is_verified": false,
"line_number": 284,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "3dd635a808ddb6dd4b6731f7c409d53dd4b14df2",
"is_verified": false,
"line_number": 356,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "67a74306b06d0c01624fe0d0249a570f4d093747",
"is_verified": false,
"line_number": 374,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "61d6504733ca7757e259c644acd085c4dd471019",
"is_verified": false,
"line_number": 910,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "4ea872dfd7eefbde0036da7f0780826353dc7477",
"is_verified": false,
"line_number": 940,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "b214f706bb602c1cc2adc5c6165e73622305f4bb",
"is_verified": false,
"line_number": 1010,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "5cbabd43e49a1fedbbc3b86311aa6c8fe446abf9",
"is_verified": false,
"line_number": 1195,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "18ad10fd4a67f21fc07b1aa5046b410f6b2bedf1",
"is_verified": false,
"line_number": 1213,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "10470c3b4b1fed12c3baac014be15fac67c6e815",
"is_verified": false,
"line_number": 1263,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "65e1946c8f102eca8ba0af291f7c5e807516d94c",
"is_verified": false,
"line_number": 1346,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "0075df0a74c07ee295c98238c018401c9a80183b",
"is_verified": false,
"line_number": 1397,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "ca0023d7b345802fbc227b902cb9c57a3e02195f",
"is_verified": false,
"line_number": 1442,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "c8c6ca2e11c2dfd2a40914585b5944bffea15c8c",
"is_verified": false,
"line_number": 1555,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "b85b97a99eab8c809570c61d6404c1e49bdefbb4",
"is_verified": false,
"line_number": 1596,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "dec7dd342a499dfd4d283d872ccf598d8a7b6039",
"is_verified": false,
"line_number": 1789,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "2dc5053699a351121bf839c446bd4a878dda5735",
"is_verified": false,
"line_number": 1939,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "e5d54f0ac13abbdaa94b696c2469148b96dd11ab",
"is_verified": false,
"line_number": 2242,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "6059f42e2bbae78141e8a9e6286755ee691d5ce0",
"is_verified": false,
"line_number": 2305,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "fe703d258c7ef5f50b71e06565a65aa07194907f",
"is_verified": false,
"line_number": 2348,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "c229b68e1c3ffd9874838b5cb5354a0ee1367ddc",
"is_verified": false,
"line_number": 2349,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "756de479126e911b6f3400ae686d663d9d26b509",
"is_verified": false,
"line_number": 2920,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "6b174322afcdb440ee9cc3cc11eb16f9a00dec04",
"is_verified": false,
"line_number": 2975,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "9860783bfb510cbb2bf34471ec0b84a7ea587695",
"is_verified": false,
"line_number": 3359,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "b227cbd22eaa96019ebfc4aff35ad2add2a47439",
"is_verified": false,
"line_number": 3590,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "381d48209aecab8834eb495c5b5406100da07882",
"is_verified": false,
"line_number": 3811,
"is_secret": false
},
{
"type": "Hex High Entropy String",
"filename": "app/main/_commonly_used_passwords.py",
"hashed_secret": "508b38590a90d32990aadd7350d160b795c3ab41",
"is_verified": false,
"line_number": 3850,
"is_secret": false
}
],
"deploy-config/sandbox.yml": [
{
"type": "Secret Keyword",
"filename": "deploy-config/sandbox.yml",
"hashed_secret": "113151dd10316fcb0d5507b6215d78e2f3fe9e54",
"is_verified": false,
"line_number": 9,
"is_secret": false
}
],
"pytest.ini": [
{
"type": "Secret Keyword",
"filename": "pytest.ini",
"hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc",
"is_verified": false,
"line_number": 7,
"is_secret": false
},
{
"type": "Base64 High Entropy String",
"filename": "pytest.ini",
"hashed_secret": "d347784b1ab6074a65cda7bc42f1561bed85493f",
"is_verified": false,
"line_number": 7,
"is_secret": false
},
{
"type": "Base64 High Entropy String",
"filename": "pytest.ini",
"hashed_secret": "ed1754d5cc82c8fd83205ebfb8c43fe4e88415a4",
"is_verified": false,
"line_number": 9,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "pytest.ini",
"hashed_secret": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
"is_verified": false,
"line_number": 11,
"is_secret": false
}
],
"tests/__init__.py": [
{
"type": "Secret Keyword",
"filename": "tests/__init__.py",
"hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea",
"is_verified": false,
"line_number": 389,
"is_secret": false
}
],
"tests/app/main/forms/test_register_user_form.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/forms/test_register_user_form.py",
"hashed_secret": "8c6c978dc8e08771c7dea1ea2370fdf2446e5ba5",
"is_verified": false,
"line_number": 38,
"is_secret": false
}
],
"tests/app/main/test_errorhandlers.py": [
{
"type": "Base64 High Entropy String",
"filename": "tests/app/main/test_errorhandlers.py",
"hashed_secret": "005fa73b3f2be8f0d71d361c1f0a9d787cd09b4e",
"is_verified": false,
"line_number": 34,
"is_secret": false
}
],
"tests/app/main/test_request_header.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/test_request_header.py",
"hashed_secret": "6866ef97a972ba3a2c6ff8bb2812981054770162",
"is_verified": false,
"line_number": 21,
"is_secret": false
}
],
"tests/app/main/views/organizations/test_organization_invites.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/organizations/test_organization_invites.py",
"hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473",
"is_verified": false,
"line_number": 274,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/organizations/test_organization_invites.py",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 282,
"is_secret": false
}
],
"tests/app/main/views/test_accept_invite.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_accept_invite.py",
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
"is_verified": false,
"line_number": 632,
"is_secret": false
}
],
"tests/app/main/views/test_new_password.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_new_password.py",
"hashed_secret": "a41d5c3bbcd0b39c627b9cbf4897c6d25efa694f",
"is_verified": false,
"line_number": 89,
"is_secret": false
}
],
"tests/app/main/views/test_register.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "bdbb156d25d02fd7792865824201dda1c60f4473",
"is_verified": false,
"line_number": 115,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 185,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_register.py",
"hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8",
"is_verified": false,
"line_number": 256,
"is_secret": false
}
],
"tests/app/main/views/test_sign_in.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_sign_in.py",
"hashed_secret": "8b8b69116ee882b5e987e330f55db81aba0636f9",
"is_verified": false,
"line_number": 95,
"is_secret": false
}
],
"tests/app/main/views/test_two_factor.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_two_factor.py",
"hashed_secret": "dc66ad927c29e31c6c374231f57a4684b0687bfe",
"is_verified": false,
"line_number": 290,
"is_secret": false
}
],
"tests/app/main/views/test_user_profile.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_user_profile.py",
"hashed_secret": "8072d7aad32964ec43fbcb699c75dc38890792f7",
"is_verified": false,
"line_number": 336,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_user_profile.py",
"hashed_secret": "4c9dbb972da179e4f66f023eaa5fb9451d835030",
"is_verified": false,
"line_number": 337,
"is_secret": false
}
],
"tests/app/main/views/test_verify.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/main/views/test_verify.py",
"hashed_secret": "faafcfa63e128929409bf310b7ea5a415f2331ce",
"is_verified": false,
"line_number": 160,
"is_secret": false
}
],
"tests/app/notify_client/test_user_client.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/notify_client/test_user_client.py",
"hashed_secret": "f2c57870308dc87f432e5912d4de6f8e322721ba",
"is_verified": false,
"line_number": 56,
"is_secret": false
}
],
"tests/app/test_cloudfoundry_config.py": [
{
"type": "Secret Keyword",
"filename": "tests/app/test_cloudfoundry_config.py",
"hashed_secret": "5e44dae2de8b6e57c797b968035265c9f2cd2b3e",
"is_verified": false,
"line_number": 12,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "tests/app/test_cloudfoundry_config.py",
"hashed_secret": "e5e178db7317356946d13e5d2da037d39ac61c71",
"is_verified": false,
"line_number": 27,
"is_secret": false
}
],
"tests/conftest.py": [
{
"type": "Secret Keyword",
"filename": "tests/conftest.py",
"hashed_secret": "f8377c90fcfd699f0ddbdcb30c2c9183d2d933ea",
"is_verified": false,
"line_number": 3287,
"is_secret": false
}
]
},
"generated_at": "2025-08-21T16:06:57Z"
}

View File

@@ -64,17 +64,6 @@ body:
validations:
required: false
- type: markdown
attributes:
value: '**Accessibility:**'
- type: textarea
id: accessibility
attributes:
label: "List any specific accessibility guidance or tests that need to be considered for this user story."
description: "List what type of accessibility tests need to pass."
validations:
required: false
- type: markdown
attributes:
value: '**Notes:**'

View File

@@ -1,17 +1,18 @@
name: "QA Checklist"
description: "Use this template to manually QA for prod push"
title: "QA checklist for production release: [insert planned release date here]"
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
body:
- type: checkboxes
id: sign_in
id: Logging_in
attributes:
label: Sign In
label: Log In
options:
- label: notify-staging.app.cloud.gov resolves
- label: Clicking sign in takes you to Login.gov
- label: Can sign in via Login.gov
- label: After successful login, you are brought to the “choose service” screen
- label: Clicking sign in takes you to login page
- label: 2FA code is sent promptly
- label: 2FA code input screen appears as expected
- label: After code is entered, you are brought to the “choose service” screen
- label: Choosing a service brings you to the dashboard for that service
- type: checkboxes
@@ -25,8 +26,9 @@ body:
- 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
id: send__copied_template
attributes:
label: Send a message from copied template
options:
@@ -36,7 +38,7 @@ body:
- type: checkboxes
id: send_existing_template
id: send__existing_template
attributes:
label: Send a message from existing template
options:
@@ -44,25 +46,13 @@ body:
- label: All else above persists as true (formatting, send, dashboard, etc)
- type: checkboxes
id: template_folder_permissions
id: download_report
attributes:
label: Checking template folder permissions
label: Download the csv delivery report
options:
- label: Only the template creator and platform admins should be able to see a new template folder
- label: Adding a non platform admin user to a folder should grant them access to see and copy the templates
- label: Removing a non platform admin user to a folder should remove their access to see and copy the templates
- label: Platform admins should not be able to be added or removed from template folders, since they have full access regardless
- type: checkboxes
id: download_reports
attributes:
label: Download the CSV delivery reports
options:
- label: Clicking on the CSV download directly after a message send downloads a report that contains the data we expect
- label: Clicking on the CSV download links from the main dashboard downloads a report that contains the data we expect
- label: The daily generated batch reports appear at the bottom of the main dashboard batch table with accurate information
- label: The daily generated batch report buttons are highlighted appropriately based on which reports are available (1, 3, 5, and 7 day)
- label: Downloading the generated batch reports that are available work properly and contain the data that we expect
- 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
@@ -82,10 +72,11 @@ body:
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that persons profile
- label: Clicking on “Remove this team member” prompts a confirmation
- label: Clicking the confirmation removes the team member
- type: checkboxes
id: sign_out
attributes:
label: Sign Out
options:
- label: Clicking “Sign Out” takes you back to the homepage and logs you out
- label: Hitting “Sign Out” takes you back to the homepage and logs you out

View File

@@ -1,9 +1,6 @@
name: Deploy egress proxy
description: Set egress space security groups and deploy proxy
inputs:
cf_org:
description: The org the target app exists in.
required: true
cf_space:
description: The space the target app exists in.
required: true
@@ -19,19 +16,6 @@ inputs:
runs:
using: composite
steps:
- name: Install cf-cli
shell: bash
run: |
curl -A "cg-deploy-action" -v -L -o cf-cli_amd64.deb 'https://packages.cloudfoundry.org/stable?release=debian64&version=v8&source=github'
sudo dpkg -i cf-cli_amd64.deb
- name: Login to cf-cli
shell: bash
run: |
cf api api.fr.cloud.gov
cf auth
- name: Target org and space
shell: bash
run: cf target -o ${{ inputs.cf_org }} -s ${{ inputs.cf_space }}
- name: Set restricted space egress
shell: bash
run: ./terraform/set_space_egress.sh -t -s ${{ inputs.cf_space }}

View File

@@ -9,19 +9,13 @@ runs:
sudo apt-get update \
&& sudo apt-get install -y --no-install-recommends \
libcurl4-openssl-dev
- name: Set up Python 3.12.9
- name: Set up Python 3.12
uses: actions/setup-python@v4
with:
python-version: "3.12.9"
python-version: "3.12"
- name: Install poetry
shell: bash
run: pip install poetry==2.1.3
- name: Install poetry export plugin
shell: bash
run: poetry self add poetry-plugin-export
- name: Downgrade virtualenv to compatible version
shell: bash
run: pip install "virtualenv<20.30"
run: pip install poetry
- name: Install application dependencies
shell: bash
run: make bootstrap

View File

@@ -3,18 +3,14 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
version: 3
updates:
- package-ecosystem: 'pip' # See documentation for possible values
directory: '/' # Location of package manifests
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: 'daily'
assignees:
- 'alexjanousekGSA'
interval: "daily"
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
versioning-strategy: increase
assignees:
- 'alexjanousekGSA'

View File

@@ -1,29 +1,79 @@
<!--
Not sure what you should include or write in a pull request? Please read the
[pull request documentation in our docs!](https://github.com/GSA/notifications-api/blob/main/docs/all.md#pull-requests)
Please follow the instructions found in this pull request template so that we
have all of the relevant details needed for our work.
At the minimum, please be sure to fill in all sections found below and also do
the following:
- Provide an appropriate and descriptive title for the pull request
- Link the pull request to its corresponding issue (must be done after creating
the pull request itself)
- Assign yourself as the author
- Attach the appropriate labels to it
- Set it to be on the Notify.gov project board
- Select one or more reviewers from the team or mark the pull request as a draft
depending on its current state
- If the pull request is a draft, please be sure to add reviewers once it is
ready for review and mark it ready for review
For each section, please delete the instructions/sample text (that includes this
text, though it is wrapped in an HTML comment just in case) and put in your own
information. Thank you!
-->
*A note to PR reviewers: it may be helpful to review our [code review documentation](https://github.com/GSA/notifications-api/blob/main/docs/all.md#code-reviews) to know what to keep in mind while reviewing pull requests.*
*A note to PR reviewers: it may be helpful to review our
[code review documentation](https://github.com/GSA/notifications-api/blob/main/docs/all.md#code-reviews)
to know what to keep in mind while reviewing pull requests.*
## Description
Please enter a detailed description here.
Please enter a clear description about your proposed changes and what the
expected outcome(s) is/are from there. If there are complex implementation
details within the changes, this is a great place to explain those details using
plain language.
This should include:
- Links to issues that this PR addresses
- Screenshots or screen captures of any visible changes, especially for UI work
- Dependency changes
If there are any caveats, known issues, follow-up items, etc., make a quick note
of them here as well, though more details are probably warranted in the issue
itself in this case.
## TODO (optional)
* [ ] TODO item 1
* [ ] TODO item 2
* [ ] TODO item ...
If you're opening a draft PR, it might be helpful to list any outstanding work,
especially if you're asking folks to take a look before it's ready for full
review. In this case, create a small checklist with the outstanding items:
- [ ] TODO item 1
- [ ] TODO item 2
- [ ] TODO item ...
## Security Considerations
* Consideration 1
* Consideration 2
* Consideration ...
Please think about the security compliance aspect of your changes and what the
potential impacts might be.
## A11y Checks (if applicable)
**NOTE: Please be mindful of sharing sensitive information here! If you're not
sure of what to write, please ask the team first before writing anything here.**
* Double check work is getting picked up by the automated E2E tests
* Conduct browser-based tests through [AxeDevTools](https://www.deque.com/axe/devtools/) and [WAVE](https://wave.webaim.org/)
* Review the [Manual Checklist](https://docs.google.com/document/d/192bBXStebdXWtYhZQ73qaWMJhGcuSB1W6c9YBXhWZvc/edit?usp=sharing)
* Make sure there are no linting errors in VSCode or other IDE of choice
Relevant details could include (and are not limited to) the following:
- Handling secrets/credential management (or specifically calling out that there
is nothing to handle)
- Any adjustments to the flow of data in and out the system, or even within it
- Connecting or disconnecting any external services to the application
- Handling of any sensitive information, such as PII
- Handling of information within log statements or other application monitoring
services/hooks
- The inclusion of a new external dependency or the removal of an existing one
- ... (anything else relevant from a security compliance perspective)
There are some cases where there are no security considerations to be had, e.g.,
updating our documentation with publicly available information. In those cases
it is fine to simply put something like this:
- None; this is a documentation update with publicly available information.

View File

@@ -10,7 +10,7 @@ env:
FLASK_APP: application.py
WERKZEUG_DEBUG_PIN: off
REDIS_ENABLED: 0
NODE_VERSION: 22.3.0
NODE_VERSION: 16.15.1
AWS_US_TOLL_FREE_NUMBER: "+18556438890"
ADMIN_BASE_URL: http://localhost:6012
@@ -23,12 +23,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22.3.0"
- name: Install dependencies
run: npm install
- uses: ./.github/actions/setup-project
- uses: jwalton/gh-find-current-pr@v1
id: findPr
@@ -38,12 +32,14 @@ jobs:
output: report-markdown
annotations: failed-tests
prnumber: ${{ steps.findPr.outputs.number }}
- name: Check imports alphabetized
run: poetry run isort --check-only ./app ./tests
- name: Run style checks
run: poetry run flake8 .
- name: Check imports alphabetized
run: poetry run isort --check-only ./app ./tests
- name: Check dead code
run: make dead-code
- name: Run js lint
run: npm run lint
- name: Run js tests
run: npm test
- name: Run py tests with coverage
@@ -52,14 +48,11 @@ jobs:
run: poetry run coverage report --fail-under=90
end-to-end-tests:
if: ${{ github.actor != 'dependabot[bot]' }}
permissions:
checks: write
pull-requests: write
contents: write
runs-on: ubuntu-latest
environment: staging
services:
postgres:
image: postgres
@@ -85,31 +78,43 @@ jobs:
ports:
# Maps tcp port 6379 on service container to the host
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
- uses: jwalton/gh-find-current-pr@v1
id: findPr
- name: Check API Server availability
run: |
curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
- name: Run Admin server
# If we want to log stuff and see what's broken,
# insert this line:
# tail -f admin-server.log &
# above make e2e-test
run: |
make run-flask > admin-server.log 2>&1 &
make wait-for-flask
tail -f admin-server.log &
make e2e-test
- name: Clone API
uses: actions/checkout@v4
with:
repository: GSA/notifications-api
path: 'notifications-api'
- name: Install API dependencies
working-directory: 'notifications-api'
run: make bootstrap
env:
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
SECRET_KEY: ${{ secrets.SECRET_KEY }}
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
REDIS_URL: redis://localhost:6379
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_ENVIRONMENT: development
- name: Run API server
working-directory: 'notifications-api'
run: make run-procfile &
env:
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
REDIS_URL: redis://localhost:6379
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_ENVIRONMENT: development
- name: Run Admin server
run: make run-flask &
env:
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov
API_HOST_NAME: http://localhost:6011
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
ADMIN_CLIENT_USERNAME: notify-admin
NOTIFY_ENVIRONMENT: e2etest
@@ -117,7 +122,19 @@ jobs:
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_URI: http://localhost:6012
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
- name: Run E2E tests
# Run the E2E tests against the code found in this PR.
# run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
# --browser webkit doesn't work at this time.
run: make e2e-test
# Debugging for now to troubleshoot a connectivity issue to the local servers
# run: curl --request GET --url "http://localhost:6012"
env:
NOTIFY_ENVIRONMENT: e2etest
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_URI: http://localhost:6012
validate-new-relic-config:
runs-on: ubuntu-latest
@@ -139,12 +156,10 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
- uses: pypa/gh-action-pip-audit@v1.1.0
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.0.6
with:
inputs: requirements.txt
ignore-vulns: |
PYSEC-2023-312
- name: Run npm audit
run: make npm-audit
@@ -165,19 +180,18 @@ jobs:
run: make run-flask &
env:
NOTIFY_ENVIRONMENT: scanning
FEATURE_SOCKET_ENABLED: true
- name: Run OWASP Baseline Scan
uses: zaproxy/action-baseline@v0.14.0
uses: zaproxy/action-baseline@v0.9.0
with:
docker_name: "ghcr.io/zaproxy/zaproxy:weekly"
target: "http://localhost:6012"
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
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-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project

View File

@@ -1,95 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main", "production" ]
pull_request:
branches: [ "main", "production" ]
schedule:
- cron: '18 5 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -16,7 +16,7 @@ env:
FLASK_APP: application.py
WERKZEUG_DEBUG_PIN: off
REDIS_ENABLED: 0
NODE_VERSION: 22.3.0
NODE_VERSION: 16.15.1
jobs:
dependency-audits:
@@ -25,12 +25,10 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
- uses: pypa/gh-action-pip-audit@v1.1.0
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- uses: pypa/gh-action-pip-audit@v1.0.6
with:
inputs: requirements.txt
ignore-vulns: |
PYSEC-2023-312
- name: Run npm audit
run: make npm-audit
@@ -52,7 +50,7 @@ jobs:
env:
NOTIFY_ENVIRONMENT: scanning
- name: Run OWASP Full Scan
uses: zaproxy/action-full-scan@v0.12.0
uses: zaproxy/action-full-scan@v0.7.0
with:
docker_name: 'ghcr.io/zaproxy/zaproxy:weekly'
target: 'http://localhost:6012'

View File

@@ -16,21 +16,23 @@ jobs:
with:
fetch-depth: 2
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v41.0.0
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
files: |
terraform/demo
terraform/shared
.github/workflows/deploy-demo.yml
- name: Terraform init
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/demo
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
run: terraform init
- name: Terraform apply
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/demo
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
@@ -42,15 +44,14 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Deploy to cloud.gov
uses: cloud-gov/cg-cli-tools@main
uses: 18f/cg-deploy-action@main
env:
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
ADMIN_CLIENT_USERNAME: "notify-admin"
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
COMMIT_HASH: ${{ github.sha }}
@@ -62,24 +63,21 @@ jobs:
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-demo.app.cloud.gov/sign-out"
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
cf_org: gsa-tts-benefits-studio
cf_space: notify-demo
cf_command: >-
push -f manifest.yml
push_arguments: >-
--vars-file deploy-config/demo.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 ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
--var COMMIT_HASH="$COMMIT_HASH"
--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"
@@ -87,18 +85,18 @@ jobs:
--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"
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
--var LOGIN_PEM="$LOGIN_PEM"
--var API_PUBLIC_URL="$API_PUBLIC_URL"
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
--strategy rolling
- name: Deploy egress proxy
uses: ./.github/actions/deploy-proxy
env:
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
CF_PASSWORD: ${{ secrets.CLOUDGOV_PASSWORD }}
- 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-demo.*.acl
.github/actions/deploy-proxy/action.yml
.github/workflows/deploy-demo.yml
- name: Deploy egress proxy
if: steps.changed-egress-config.outputs.any_changed == 'true'
uses: ./.github/actions/deploy-proxy
with:
cf_org: gsa-tts-benefits-studio
cf_space: notify-demo
app: notify-admin-demo

View File

@@ -16,21 +16,23 @@ jobs:
with:
fetch-depth: 2
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v41.0.0
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
files: |
terraform/production
terraform/shared
.github/workflows/deploy-prod.yml
- name: Terraform init
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
run: terraform init
- name: Terraform apply
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
@@ -42,15 +44,14 @@ jobs:
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Deploy to cloud.gov
uses: cloud-gov/cg-cli-tools@main
uses: 18f/cg-deploy-action@main
env:
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
ADMIN_CLIENT_USERNAME: "notify-admin"
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
COMMIT_HASH: ${{ github.sha }}
@@ -62,24 +63,21 @@ jobs:
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://beta.notify.gov/sign-out"
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
cf_org: gsa-tts-benefits-studio
cf_space: notify-production
cf_command: >-
push -f manifest.yml
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 ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
--var COMMIT_HASH="$COMMIT_HASH"
--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"
@@ -87,18 +85,18 @@ jobs:
--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"
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
--var LOGIN_PEM="$LOGIN_PEM"
--var API_PUBLIC_URL="$API_PUBLIC_URL"
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
--strategy rolling
- name: Deploy egress proxy
uses: ./.github/actions/deploy-proxy
env:
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
CF_PASSWORD: ${{ secrets.CLOUDGOV_PASSWORD }}
- 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_org: gsa-tts-benefits-studio
cf_space: notify-production
app: notify-admin-production

View File

@@ -17,98 +17,96 @@ jobs:
environment: staging
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/checkout@v4
with:
fetch-depth: 2
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for changes to Terraform
id: changed-terraform-files
uses: tj-actions/changed-files@v41.0.0
with:
files: |
terraform/staging
terraform/shared
.github/workflows/deploy.yml
- name: Terraform init
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/staging
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
run: terraform init
- name: Terraform apply
if: steps.changed-terraform-files.outputs.any_changed == 'true'
working-directory: terraform/staging
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
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
- name: Terraform init
working-directory: terraform/staging
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
run: terraform init
- name: Terraform apply
working-directory: terraform/staging
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
- uses: ./.github/actions/setup-project
- name: Create requirements.txt
run: poetry export --output requirements.txt
- name: Create requirements.txt
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: Deploy to cloud.gov
uses: cloud-gov/cg-cli-tools@main
env:
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
ADMIN_CLIENT_USERNAME: "notify-admin"
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
COMMIT_HASH: ${{ github.sha }}
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://secure.login.gov/api/openid_connect/userinfo"
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://secure.login.gov/api/openid_connect/token"
LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.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://secure.login.gov/openid_connect/logout?"
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
API_PUBLIC_URL: ${{ secrets.API_PUBLIC_URL }}
API_PUBLIC_WS_URL: ${{ secrets.API_PUBLIC_WS_URL }}
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
cf_org: gsa-tts-benefits-studio
cf_space: notify-staging
cf_command: >-
push -f manifest.yml
--vars-file deploy-config/staging.yml
--var DANGEROUS_SALT="$DANGEROUS_SALT"
--var SECRET_KEY="$SECRET_KEY"
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
--var ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
--var COMMIT_HASH="$COMMIT_HASH"
--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"
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
--var LOGIN_PEM="$LOGIN_PEM"
--var API_PUBLIC_URL="$API_PUBLIC_URL"
--var API_PUBLIC_WS_URL="$API_PUBLIC_WS_URL"
--strategy rolling
- name: 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 }}
COMMIT_HASH: ${{ github.sha }}
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://secure.login.gov/api/openid_connect/userinfo"
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://secure.login.gov/api/openid_connect/token"
LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.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://secure.login.gov/openid_connect/logout?"
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATEE"
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
cf_org: gsa-tts-benefits-studio
cf_space: notify-staging
push_arguments: >-
--vars-file deploy-config/staging.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 COMMIT_HASH="$COMMIT_HASH"
--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: Deploy egress proxy
uses: ./.github/actions/deploy-proxy
env:
CF_USERNAME: ${{ secrets.CLOUDGOV_USERNAME }}
CF_PASSWORD: ${{ secrets.CLOUDGOV_PASSWORD }}
with:
cf_org: gsa-tts-benefits-studio
cf_space: notify-staging
app: notify-admin-staging
- 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-staging.*.acl
.github/actions/deploy-proxy/action.yml
.github/workflows/deploy.yml
- name: Deploy egress proxy
if: steps.changed-egress-config.outputs.any_changed == 'true'
uses: ./.github/actions/deploy-proxy
with:
cf_space: notify-staging
app: notify-admin-staging
bail:
runs-on: ubuntu-latest

View File

@@ -15,35 +15,15 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for drift
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 }}
run: |
cd terraform/staging
terraform init
terraform plan -detailed-exitcode
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "No changes detected. Intrastructure is up-to-date."
elif [ $exit_code -eq 2 ]; then
echo "Changes detected. Infrastructure drift found."
exit 1
else
echo "Error running terraform plan."
exit $exit_code
fi
with:
path: terraform/staging
check_demo_drift:
runs-on: ubuntu-latest
@@ -55,34 +35,15 @@ jobs:
with:
ref: 'production'
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for drift
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 }}
run: |
cd terraform/demo
terraform init
terraform plan -detailed-exitcode
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "No changes detected. Intrastructure is up-to-date."
elif [ $exit_code -eq 2 ]; then
echo "Changes detected. Infrastructure drift found."
exit 1
else
echo "Error running terraform plan."
exit $exit_code
fi
with:
path: terraform/demo
check_prod_drift:
runs-on: ubuntu-latest
@@ -94,31 +55,12 @@ jobs:
with:
ref: 'production'
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Check for drift
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 }}
run: |
cd terraform/production
terraform init
terraform plan -detailed-exitcode
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "No changes detected. Intrastructure is up-to-date."
elif [ $exit_code -eq 2 ]; then
echo "Changes detected. Infrastructure drift found."
exit 1
else
echo "Error running terraform plan."
exit $exit_code
fi
with:
path: terraform/production

View File

@@ -18,14 +18,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Terraform format
id: format
run: terraform fmt -check
@@ -59,7 +51,7 @@ jobs:
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
- name: Update PR
uses: actions/github-script@v7
uses: actions/github-script@v6
# we would like to update the PR even when a prior step failed
if: ${{ always() }}
with:

View File

@@ -18,14 +18,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Terraform format
id: format
run: terraform fmt -check
@@ -59,7 +51,7 @@ jobs:
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
- name: Update PR
uses: actions/github-script@v7
uses: actions/github-script@v6
# we would like to update the PR even when a prior step failed
if: ${{ always() }}
with:

View File

@@ -18,14 +18,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Looks like we need to install Terraform ourselves now!
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "^1.7.5"
terraform_wrapper: false
- name: Terraform format
id: format
run: terraform fmt -check
@@ -59,7 +51,7 @@ jobs:
# inspiration: https://learn.hashicorp.com/tutorials/terraform/github-actions#review-actions-workflow
- name: Update PR
uses: actions/github-script@v7
uses: actions/github-script@v6
# we would like to update the PR even when a prior step failed
if: ${{ always() }}
with:

17
.gitignore vendored
View File

@@ -14,16 +14,6 @@
*.XLSX
## Non user files allowed to be commited
!app/assets/pdf/wa_case_study.pdf
!app/assets/pdf/best-practices-for-texting-the-public.pdf
!app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf
!app/assets/pdf/best-practices-section-outline.pdf
!app/assets/pdf/standing-up-your-own-notify.pdf
!app/assets/pdf/tcpa_overview.pdf
!app/assets/pdf/investing-notifications-tts-public-benefits-memo.pdf
!app/assets/pdf/out-of-pilot-announcement.pdf
!app/assets/pdf/studio-research-snapshot-2022-07-external.pdf
!app/assets/pdf/TCPA-Overview.pdf
!tests/test_pdf_files/no_eof_marker.pdf
!tests/test_pdf_files/multi_page_pdf.pdf
!tests/test_pdf_files/big.pdf
@@ -133,8 +123,6 @@ app/templates/vendor
secrets.auto.tfvars
terraform.tfstate
terraform.tfstate.backup
requirements.txt
terraform/sandbox/requirements.txt
# Playwright
playwright/
@@ -144,8 +132,3 @@ playwright/
# Nodenv
.node-version
# BackstopJS
backstop_data/bitmaps_test/
backstop_data/html_report/
backstop_data/engine_scripts/playwright/

2
.nvmrc
View File

@@ -1 +1 @@
22.3.0
16.15.1

View File

@@ -1,10 +1,7 @@
{
"defaults": {
"standard": "WCAG2AA",
"runners": ["htmlcs"],
"chromeLaunchConfig": {
"executablePath": "/usr/bin/google-chrome"
},
"runners": ["htmlcs"],
"concurrency": 1,
"hideElements": [
"nav > ol a",

View File

@@ -2,24 +2,12 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
exclude: ^app/assets/pdf/.*\.pdf$
- id: debug-statements
- id: check-merge-conflict
- id: check-toml
- id: check-ast
- id: fix-byte-order-marker
- id: detect-aws-credentials
args: [--allow-missing-credentials]
- id: detect-private-key
- id: mixed-line-ending
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.ds.baseline']

View File

@@ -1,3 +0,0 @@
{
"singleQuote": true
}

View File

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

View File

@@ -6,7 +6,6 @@ APP_VERSION_FILE = app/version.py
GIT_BRANCH ?= $(shell git symbolic-ref --short HEAD 2> /dev/null || echo "detached")
GIT_COMMIT ?= $(shell git rev-parse HEAD 2> /dev/null || echo "")
GIT_HOOKS_PATH ?= $(shell git config --global core.hooksPath || echo "")
VIRTUALENV_ROOT := $(shell [ -z $$VIRTUAL_ENV ] && echo $$(pwd)/venv || echo $$VIRTUAL_ENV)
@@ -15,25 +14,14 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
## DEVELOPMENT
.PHONY: bootstrap
bootstrap: ## Set up everything to run the application
make generate-version-file
poetry sync --no-root
bootstrap: generate-version-file ## Set up everything to run the app
poetry self update
poetry self add poetry-dotenv-plugin
poetry lock --no-update
poetry install --sync --no-root
poetry run playwright install --with-deps
poetry run pre-commit install
source $(NVMSH) --no-use && nvm install && npm install
source $(NVMSH) && npm ci --no-audit
source $(NVMSH) && npm run build
.PHONY: bootstrap-with-git-hooks
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
make generate-version-file
poetry sync --no-root
poetry run playwright install --with-deps
git config --global --unset-all core.hooksPath
poetry run pre-commit install
git config --global core.hookspath "${GIT_HOOKS_PATH}"
source $(NVMSH) --no-use && nvm install && npm install
source $(NVMSH) && npm ci --no-audit
source $(NVMSH) --no-use && nvm install && npm ci --no-audit
source $(NVMSH) && npm run build
.PHONY: watch-frontend
@@ -44,19 +32,6 @@ watch-frontend: ## Build frontend and watch for changes
run-flask: ## Run flask
poetry run newrelic-admin run-program flask run -p 6012 --host=0.0.0.0
.PHONY: wait-for-flask
wait-for-flask:
@echo "Waiting for Flask to start..."
@timeout 30 bash -c 'until curl -sf http://localhost:6012 > /dev/null 2>&1; do sleep 1; done'
@echo "Flask is ready!"
.PHONY: run-flask-and-wait
run-flask-and-wait:
@make run-flask &
@echo "Waiting for Flask to start..."
@timeout 30 bash -c 'until curl -sf http://localhost:6012 > /dev/null 2>&1; do sleep 1; done'
@echo "Flask is ready!"
.PHONY: run-flask-bare
run-flask-bare: ## Run flask without invoking poetry so we can override ENV variables in .env
flask run -p 6012 --host=0.0.0.0
@@ -65,10 +40,6 @@ run-flask-bare: ## Run flask without invoking poetry so we can override ENV var
npm-audit: ## Check for vulnerabilities in NPM packages
source $(NVMSH) && npm run audit
.PHONY: npm-audit-fix
npm-audit-fix: ## Fix vulnerabilities that do not require attentino (according to npm)
source $(NVMSH) && npm audit fix
.PHONY: help
help:
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@@ -78,10 +49,7 @@ generate-version-file: ## Generates the app version file
@echo -e "__git_commit__ = \"${GIT_COMMIT}\"\n__time__ = \"${DATE}\"" > ${APP_VERSION_FILE}
.PHONY: test
test: py-lint py-test js-test ## Run tests
.PHONY: test-fast
test-fast: py-lint py-test-fast js-test ## Run tests quickly in parallel (testing locally first)
test: py-lint py-test js-lint js-test ## Run tests
.PHONY: py-lint
py-lint: ## Run python linting scanners and black
@@ -90,13 +58,6 @@ py-lint: ## Run python linting scanners and black
poetry run flake8 .
poetry run isort --check-only ./app ./tests
.PHONY: tada
tada: ## Run python linting scanners and black
poetry run isort ./app ./tests
poetry run black .
poetry run flake8 .
.PHONY: avg-complexity
avg-complexity:
echo "*** Shows average complexity in radon of all code ***"
@@ -110,24 +71,18 @@ too-complex:
.PHONY: py-test
py-test: export NEW_RELIC_ENVIRONMENT=test
py-test: ## Run python unit tests
poetry run coverage run -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
poetry run coverage report --fail-under=93
poetry run coverage 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: py-test-fast
py-test-fast: export NEW_RELIC_ENVIRONMENT=test
py-test-fast: ## Run python unit tests in parallel (testing locally first)
poetry run pytest --maxfail=10 --ignore=tests/end_to_end tests/ -n auto
.PHONY: dead-code
dead-code: ## 60% is our aspirational goal, but currently breaks the build
poetry run vulture ./app ./notifications_utils --min-confidence=100
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; note that --browser webkit isn't currently working
@echo "Running E2E tests in path: $${TESTPATH:-tests/end_to_end}"
@bash -c 'DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox "$${TESTPATH:-tests/end_to_end}"'
poetry run pytest -v --browser chromium --browser firefox tests/end_to_end
.PHONY: js-lint
js-lint: ## Run javascript linting scanners
@@ -146,9 +101,16 @@ py-lock: ## Syncs dependencies and updates lock file without performing recursiv
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
.PHONY: freeze-requirements
freeze-requirements: ## create static requirements.txt
poetry export --output requirements.txt
poetry export --without-hashes --format=requirements.txt > requirements.txt
.PHONY: pip-audit
pip-audit:
@@ -180,7 +142,18 @@ clean:
upload-static:
aws s3 cp --region us-west-2 --recursive --cache-control max-age=315360000,immutable ./app/static s3://${DNS_NAME}-static
.PHONY: test-single
test-single: export NEW_RELIC_ENVIRONMENT=test
test-single: ## Run a single test file
poetry run pytest $(TEST_FILE)
# .PHONY: cf-failwhale-deployed
# cf-failwhale-deployed:
# @cf app notify-admin-failwhale --guid || (echo "notify-admin-failwhale is not deployed on ${CF_SPACE}" && exit 1)
# .PHONY: enable-failwhale
# enable-failwhale: cf-target cf-failwhale-deployed ## Enable the failwhale app and disable admin
# @cf map-route notify-admin-failwhale ${DNS_NAME} --hostname www
# @cf unmap-route notify-admin ${DNS_NAME} --hostname www
# @echo "Failwhale is enabled"
# .PHONY: disable-failwhale
# disable-failwhale: cf-target cf-failwhale-deployed ## Disable the failwhale app and enable admin
# @cf map-route notify-admin ${DNS_NAME} --hostname www
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
# @echo "Failwhale is disabled"

105
README.md
View File

@@ -19,6 +19,7 @@ UI's backend and is required for most things to function. Set that up first!
Our other repositories are:
- [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)
@@ -31,7 +32,7 @@ You will need the following items:
[setting up an account](https://cloud.gov/sign-up/) (requires a `.mil`,
`.gov`, or `.fed.us` email address) and getting access to the
`notify-local-dev` and `notify-staging` spaces.
- Admin privileges and SSH access on your machine; you may need to work with
- Admin priviliges and SSH access on your machine; you may need to work with
your organization's IT support staff if you're not sure or don't currently
have this access.
@@ -40,7 +41,7 @@ You will need the following items:
[Follow the instructions here to set up the Notify.gov API.](https://github.com/GSA/notifications-api#before-you-start)
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
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.
@@ -48,7 +49,7 @@ in front of them.
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 22.3.0) and the `npm` package manager.
for managing and using Node.js (version 16.15.1) and the `npm` package manager.
These instructions will walk you through how to set your machine up with all of
the required tools for this project.
@@ -83,13 +84,11 @@ 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`
@@ -99,7 +98,7 @@ 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
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
@@ -130,7 +129,7 @@ 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
- [tenv](https://github.com/tofuutils/tenv) - for managing
- [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)
@@ -147,23 +146,20 @@ 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 `tenv` to manage
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
tenv
tfenv install latest:^1.4.0
```
This will open a menu for you; choose Terraform, then choose the latest stable
version.
_NOTE: This project currently uses the latest `1.12.x release of Terraform._
_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
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:
@@ -246,12 +242,12 @@ 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.12.9` is what is installed on your
environment (assumes version Python `3.12.2` is what is installed on your
machine):
```sh
cd notifications-admin
pyenv virtualenv 3.12.9 notify-admin
pyenv virtualenv 3.12.2 notify-admin
pyenv local notify-admin
```
@@ -290,7 +286,7 @@ we'll use `3.12` in our example here since we recently upgraded to this version:
pyenv install 3.12
```
Next, delete the virtual environment you previously had set up. If you followed
Next, delete the virtual environment you previously had set up. If you followed
the instructions above with the first-time set up, you can do this with `pyenv`:
```sh
@@ -302,7 +298,7 @@ environment with the newer version of Python you just installed:
```sh
cd notifications-admin
pyenv virtualenv 3.12.9 notify-admin
pyenv virtualenv 3.12.2 notify-admin
pyenv local notify-admin
```
@@ -311,36 +307,6 @@ you'll be set with an upgraded version of Python.
_If you're not sure about the details of your current virtual environment, you can run `poetry env info` to get more information. If you've been using `pyenv` for everything, you can also see all available virtual environments with `pyenv virtualenvs`._
#### Poetry upgrades ####
If you are doing a new project setup, then after you install poetry you need to install the export plugin
```sh
poetry self add poetry-plugin-export
```
If you are upgrading from poetry 1.8.5, you need to do this:
```sh
curl -sSL https://install.python-poetry.org | python3 - --version 2.1.3
poetry self add poetry-export-plugin
```
#### Updating the .env file for Login.gov
To configure the application for Login.gov, you will need to update the following environment variables in the .env file:
```
COMMIT_HASH=”--------”
```
Reach out to someone on the team to get the most recent Login.gov key.
```
LOGIN_PEM="INSERT_LOGIN_GOV_KEY_HERE"
```
#### Updating the .env file for E2E tests
With the newly created `.env` file in place, you'll need to make one more
@@ -385,22 +351,6 @@ 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!
## Creating a 'First User' in the database
After you have completed all setup steps, you will be unable to log in, because there
will not be a user in the database to link to the login.gov account you are using. So
you will need to create that user in your database using the 'create-test-user' command.
Open a terminal pointing to the api project and then run this command.
```poetry run flask command create-test-user --admin=True```
Supply your name, email address, mobile number, and password when prompted. Make sure the email address
is the same one you are using in login.gov and make sure your phone number is in the format 5555555555.
If for any reason in the course of development it is necessary for your to delete your db
via the `dropdb` command, you will need to repeat these steps when you recreate your db.
## Git Hooks
We're using [`pre-commit`](https://pre-commit.com/) to manage hooks in order to
@@ -446,6 +396,23 @@ 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
@@ -520,13 +487,3 @@ 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)
## Updating secrets for the E2E tests
At some point, E2E tests will fail because the secrets held in VCAP_SERVICES have expired. To refresh
them, you will need to do the following:
1. Log in the normal way to access cloudfoundry command line options
2. In your terminal, run `chmod +x print_vcap.sh`
3. In your terminal, run `./print_vcap.sh`
4. Copy the value in your terminal and paste it into the VCAP_SERVICES secret in Github on the staging tier.

View File

@@ -1,2 +0,0 @@
Contact: mailto:notify-support@gsa.gov
Expires: 2035-10-15T23:59:59Z

View File

@@ -1,11 +1,8 @@
import os
import pathlib
import re
import secrets
import sys
from functools import partial
from time import monotonic
from urllib.parse import unquote, urlparse, urlunparse
from urllib.parse import urlparse, urlunparse
import jinja2
from flask import (
@@ -25,6 +22,7 @@ from flask_talisman import Talisman
from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
from itsdangerous import BadSignature
from notifications_python_client.errors import HTTPError
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from werkzeug.exceptions import abort
from werkzeug.local import LocalProxy
@@ -32,10 +30,9 @@ from werkzeug.local import LocalProxy
from app import proxy_fix
from app.asset_fingerprinter import asset_fingerprinter
from app.config import configs
from app.extensions import redis_client
from app.extensions import redis_client, zendesk_client
from app.formatters import (
convert_markdown_template,
convert_time_unixtimestamp,
convert_to_boolean,
format_auth_type,
format_billions,
@@ -49,7 +46,6 @@ from app.formatters import (
format_datetime_human,
format_datetime_normal,
format_datetime_relative,
format_datetime_scheduled_notification,
format_datetime_table,
format_day_of_week,
format_delta,
@@ -102,35 +98,27 @@ from app.notify_client.organizations_api_client import organizations_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.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.utils.api_health import is_api_down
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
from notifications_python_client.errors import HTTPError
from notifications_utils import logging, request_helper
from notifications_utils.formatters import (
formatted_list,
get_lines_with_normalised_whitespace,
)
from notifications_utils.recipients import format_phone_number_human_readable
from notifications_utils.url_safe_token import generate_token
login_manager = LoginManager()
csrf = CSRFProtect()
talisman = Talisman()
# The current service attached to the request stack.
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
@@ -148,84 +136,32 @@ navigation = {
def _csp(config):
asset_domain = config["ASSET_DOMAIN"]
api_public_url = config["API_PUBLIC_URL"]
api_public_ws_url = config["API_PUBLIC_WS_URL"]
logo_domain = config["LOGO_CDN_DOMAIN"]
return {
"default-src": ["'self'", asset_domain],
"frame-src": [
"https://www.youtube.com",
"https://www.youtube-nocookie.com",
"https://www.googletagmanager.com",
],
"frame-ancestors": "'none'",
"form-action": "'self'",
"script-src": [
"'self'",
asset_domain,
"'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",
"https://cdn.socket.io",
],
"connect-src": list(dict.fromkeys([
"connect-src": [
"'self'",
"https://gov-bam.nr-data.net",
"https://www.google-analytics.com",
f"{api_public_url}",
f"{api_public_ws_url}",
])),
],
"style-src": ["'self'", asset_domain],
"img-src": ["'self'", asset_domain],
"img-src": ["'self'", asset_domain, logo_domain],
}
def create_app(application):
@application.after_request
def add_security_headers(response):
response.headers["Cross-Origin-Embedder-Policy"] = "credentialless"
return response
@application.context_processor
def inject_feature_flags():
# this is where feature flags can be easily added as a dictionary within context
feature_socket_enabled = application.config.get("FEATURE_SOCKET_ENABLED", False)
return dict(
FEATURE_SOCKET_ENABLED=feature_socket_enabled,
)
@application.context_processor
def inject_is_api_down():
return {"is_api_down": is_api_down()}
@application.context_processor
def inject_initial_signin_url():
ttl = 24 * 60 * 60
# make and store the state
state = generate_token(
str(request.remote_addr),
current_app.config["SECRET_KEY"],
current_app.config["DANGEROUS_SALT"],
)
state_key = f"login-state-{unquote(state)}"
redis_client.set(state_key, state, ex=ttl)
# make and store the nonce
nonce = secrets.token_urlsafe()
nonce_key = f"login-nonce-{unquote(nonce)}"
redis_client.set(nonce_key, nonce, ex=ttl)
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
if url is not None:
url = url.replace("NONCE", nonce)
url = url.replace("STATE", state)
return {"initial_signin_url": url}
notify_environment = os.environ["NOTIFY_ENVIRONMENT"]
application.config.from_object(configs[notify_environment])
@@ -265,6 +201,7 @@ def create_app(application):
user_api_client,
# External API clients
redis_client,
zendesk_client,
):
client.init_app(application)
@@ -273,11 +210,14 @@ def create_app(application):
content_security_policy=_csp(application.config),
content_security_policy_nonce_in=["style-src", "script-src"],
permissions_policy={
"accelerometer": '(self "https://www.youtube-nocookie.com")',
"autoplay": '(self "https://www.youtube-nocookie.com")',
"accelerometer": "()",
"ambient-light-sensor": "()",
"autoplay": "()",
"battery": "()",
"camera": "()",
"document-domain": "()",
"geolocation": "()",
"gyroscope": '(self "https://www.youtube-nocookie.com")',
"gyroscope": "()",
"local-fonts": "()",
"magnetometer": "()",
"microphone": "()",
@@ -290,24 +230,6 @@ def create_app(application):
)
logging.init_app(application)
# Hopefully will help identify if there is a race condition causing the CSRF errors
# that we have occasionally seen in our environments.
for key in ("SECRET_KEY", "DANGEROUS_SALT"):
try:
value = application.config[key]
except KeyError:
application.logger.error(f"Env Var {key} doesn't exist.")
else:
try:
data_len = len(value.strip())
except (TypeError, AttributeError):
application.logger.error(f"Env Var {key} invalid type: {type(value)}")
else:
if data_len:
application.logger.info(f"Env Var {key} is a non-zero length.")
else:
application.logger.error(f"Env Var {key} is empty.")
login_manager.login_view = "main.sign_in"
login_manager.login_message_category = "default"
login_manager.session_protection = None
@@ -358,12 +280,6 @@ def init_app(application):
def _attach_current_user():
return {"current_user": current_user}
@application.context_processor
def _attach_enums():
from app.enums import ServicePermission
return {"ServicePermission": ServicePermission}
@application.context_processor
def _nav_selected():
return navigation
@@ -431,23 +347,11 @@ def make_session_permanent():
def create_beta_url(url):
url_created = None
try:
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
except ValueError:
# This might be happening due to IPv6, see issue # 1395.
# If we see "'RequestContext' object has no attribute 'service'" in the logs
# we can search around that timestamp and find this output, hopefully.
# It may be sufficient to just catch and log, and prevent the stack trace from being in the logs
# but we need to confirm the root cause first.
current_app.logger.error(
f"create_beta_url orig_url: {url} \
url_created = {str(url_created)} url_for_redirect {str(url_for_redirect)}"
)
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():
@@ -455,7 +359,6 @@ def redirect_notify_to_beta():
current_app.config["NOTIFY_ENVIRONMENT"] == "production"
and "beta.notify.gov" not in request.url
):
# TODO add debug here to trace what is going on with the URL for the 'RequestContext' error
url_to_beta = create_beta_url(request.url)
return redirect(url_to_beta, 302)
@@ -538,28 +441,26 @@ def register_errorhandlers(application): # noqa (C901 too complex)
@application.errorhandler(HTTPError)
def render_http_error(error):
error_url = error.response.url if error.response else "unknown URL"
application.logger.warning(
f"API {error_url} failed with status {error.status_code} message {error.message}",
exc_info=sys.exc_info(),
stack_info=True,
"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(
f"API {error_url} failed with status {error.status_code} message {error.message}",
exc_info=sys.exc_info(),
stack_info=True,
"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)
@@ -644,15 +545,12 @@ def setup_event_handlers():
def add_template_filters(application):
application.add_template_filter(slugify)
for fn in [
format_auth_type,
format_billions,
format_datetime,
format_datetime_24h,
format_datetime_normal,
format_datetime_scheduled_notification,
format_datetime_table,
valid_phone_number,
linkable_name,
@@ -680,7 +578,6 @@ def add_template_filters(application):
format_thousands,
id_safe,
convert_to_boolean,
convert_time_unixtimestamp,
format_list_items,
iteration_count,
recipient_count,
@@ -704,10 +601,3 @@ def init_jinja(application):
]
jinja_loader = jinja2.FileSystemLoader(template_folders)
application.jinja_loader = jinja_loader
def slugify(text):
"""
Converts text to lowercase, replaces spaces with hyphens, and removes invalid characters.
"""
return re.sub(r"[^a-z0-9-]", "", re.sub(r"\s+", "-", text.lower()))

View File

@@ -17,7 +17,7 @@
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="{{ asset_url('images/site.webmanifest') }}">
<link rel="manifest" href="/static/images/site.webmanifest">
<link rel="stylesheet" media="screen" href="/static/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
<link rel="stylesheet" media="print" href="/static/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
@@ -68,8 +68,11 @@
<p class="usa-body">
Try again later.
</p>
<p>To report a problem you can email us at <a class="usa-link" href="mailto:notify-support@gsa.gov">notify-support@gsa.gov</a>.</p>
<p>You can expect a response within one business day.</p>
<p class="usa-body">
You can check our <a class="usa-link"
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>
</div>
</div>
</main>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><g><path d="m377.64 347.666c-4.473 0-8.1 3.626-8.1 8.099s3.626 8.1 8.1 8.1c4.473 0 8.099-3.626 8.099-8.1s-3.626-8.099-8.099-8.099z"/><path d="m377.716 334.615c4.149 0 7.511-3.363 7.511-7.511v-67.172c0-4.148-3.362-7.511-7.511-7.511-4.148 0-7.511 3.363-7.511 7.511v67.172c0 4.148 3.362 7.511 7.511 7.511z"/><path d="m377.64 219.085c-7.974 0-15.716 1.011-23.105 2.912v-14.937c0-59.921-37.644-111.208-90.522-131.476 2.741-6.491 4.267-13.614 4.267-21.092 0-30.047-24.445-54.492-54.492-54.492s-54.491 24.445-54.491 54.491c0 7.469 1.522 14.586 4.256 21.071-16.063 6.135-30.994 15.219-44.006 26.957-3.081 2.778-3.325 7.528-.547 10.608 2.781 3.081 7.53 3.324 10.608.546 23.118-20.854 53.014-32.339 84.18-32.339 69.324 0 125.725 56.4 125.725 125.725v20.222c-32.217 14.573-54.688 47.021-54.688 84.618 0 11.997 2.288 23.47 6.45 34.005h-203.212v-138.845c0-24.475 7.036-48.196 20.346-68.599 2.267-3.474 1.288-8.128-2.187-10.394-3.474-2.266-8.129-1.288-10.394 2.186-14.907 22.851-22.787 49.41-22.787 76.807v143.829l-28.543 37.616c-1.903 2.506-2.952 5.62-2.952 8.768v27.183c0 7.996 6.505 14.501 14.501 14.501h94.92c-.468 3.102-.719 6.274-.719 9.504 0 35.036 28.504 63.541 63.541 63.541 35.036 0 63.541-28.504 63.541-63.541 0-3.232-.263-6.402-.735-9.504h29.487c4.149 0 7.511-3.363 7.511-7.511s-3.362-7.511-7.511-7.511h-249.514v-26.486l27.713-36.521h214.585c15.307 24.506 41.72 41.39 72.142 43.548v19.459h-29.946c-4.148 0-7.511 3.363-7.511 7.511s3.362 7.511 7.511 7.511h30.467c7.996 0 14.5-6.505 14.5-14.501v-20.125c47.261-4.255 84.425-44.082 84.425-92.431 0-51.178-41.636-92.814-92.814-92.814zm-127.992-148.137c-11.454-3.02-23.471-4.636-35.86-4.636-12.217 0-24.259 1.583-35.863 4.634-2.311-5.012-3.607-10.584-3.607-16.455 0-21.764 17.706-39.469 39.469-39.469s39.469 17.706 39.469 39.469c.002 5.873-1.295 11.445-3.608 16.457zm-83.427 368.007h95.144c.616 3.074.942 6.251.942 9.504 0 26.753-21.765 48.519-48.519 48.519s-48.519-21.766-48.519-48.519c.001-3.254.335-6.429.952-9.504zm211.419-49.263c-42.895 0-77.793-34.898-77.793-77.793s34.897-77.793 77.793-77.793 77.793 34.898 77.793 77.793c-.001 42.895-34.898 77.793-77.793 77.793z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m468.547 144.82h-63.047v-121.32c0-12.958-10.542-23.5-23.5-23.5h-324c-12.958 0-23.5 10.542-23.5 23.5v84.833c.344 9.931 14.659 9.923 15 0v-33.333h341v69.82h-121.998c-14.312 0-25.954 11.574-25.954 25.8v83.174l-28.866 19.221c-5.511 3.444-5.51 12.168.001 15.611l28.865 19.22v83.174c0 14.226 11.643 25.799 25.954 25.799h121.998v71.68c0 4.687-3.813 8.5-8.5 8.5h-324c-4.687 0-8.5-3.813-8.5-8.5v-20h131.5c7.444 0 13.5-6.056 13.5-13.5v-127.999c0-7.444-6.056-13.5-13.5-13.5h-131.5v-168.021c-.344-9.931-14.659-9.923-15 0v168.021h-3.5c-7.444 0-13.5 6.056-13.5 13.5v128c0 7.444 6.056 13.5 13.5 13.5h3.5v20c0 12.958 10.542 23.5 23.5 23.5h324c12.958 0 23.5-10.542 23.5-23.5v-71.68h11.5c9.931-.344 9.923-14.659 0-15h-148.498c-6.04 0-10.954-4.845-10.954-10.799v-85.173c0-3.779-1.882-7.288-5.031-9.384l-23.492-15.643 23.492-15.644c3.15-2.098 5.031-5.606 5.031-9.384v-85.173c0-5.955 4.914-10.8 10.954-10.8h200.045c6.04 0 10.953 4.845 10.953 10.8v220.401c0 5.955-4.913 10.799-10.953 10.799h-16.436c-9.931.344-9.923 14.659 0 15h16.436c14.311 0 25.953-11.574 25.953-25.799v-220.401c0-14.226-11.643-25.8-25.953-25.8zm-256.047-129.82v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.333 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.334 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-52.333 45v-36.5c0-4.687 3.813-8.5 8.5-8.5h13.833v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.334v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.333v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h154.5c4.687 0 8.5 3.813 8.5 8.5v36.5zm-17 268.5h147v125h-147z"/><path d="m451.356 344.514c2.439-4.507 2.214-9.973-.589-14.264l-69.093-105.771c-5.242-8.361-18.107-8.359-23.35 0l-69.092 105.771c-2.803 4.291-3.028 9.756-.589 14.263s7.139 7.307 12.264 7.307h138.186c5.125 0 9.824-2.799 12.263-7.306zm-148.499-7.694 67.143-102.786 67.143 102.786z"/><path d="m362.5 266.32v31.445c.344 9.931 14.659 9.923 15 0v-31.445c-.344-9.931-14.659-9.923-15 0z"/><path d="m370 310.82c-4.143 0-7.5 3.358-7.5 7.5v2.5c.344 9.931 14.659 9.923 15 0v-2.5c0-4.142-3.357-7.5-7.5-7.5z"/><path d="m91.375 123.094h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 149.316h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 175.539h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m91.375 225.76h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 251.982h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 278.205h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m139.354 356.038c-2.93-2.929-7.678-2.929-10.607 0l-22.747 22.747-22.747-22.747c-2.93-2.929-7.678-2.929-10.607 0s-2.929 7.678 0 10.606l22.748 22.748-22.748 22.748c-2.929 2.929-2.929 7.678 0 10.606 2.93 2.929 7.677 2.929 10.607 0l22.747-22.747 22.747 22.747c2.93 2.929 7.677 2.929 10.607 0 2.929-2.929 2.929-7.678 0-10.606l-22.748-22.748 22.748-22.748c2.929-2.928 2.929-7.677 0-10.606z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -1,17 +0,0 @@
<svg width="48" height="49" viewBox="0 0 48 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6378_116)">
<path d="M13.3517 3.77441H4.3344C3.40346 3.80666 3.40421 5.14865 4.3344 5.18053H13.3516C14.2826 5.14828 14.2819 3.80629 13.3517 3.77441Z" fill="black"/>
<path d="M15.8219 10.6309H5.94759C4.67037 10.6309 3.63135 11.6699 3.63135 12.9471V13.9669C3.63135 15.2441 4.67037 16.2832 5.94759 16.2832H15.8219C17.0991 16.2832 18.1381 15.2441 18.1381 13.9669V12.9471C18.1381 11.67 17.0991 10.6309 15.8219 10.6309ZM16.732 13.9669C16.732 14.4688 16.3238 14.877 15.8219 14.877H5.94759C5.4457 14.877 5.03746 14.4688 5.03746 13.9669V12.9471C5.03746 12.4452 5.4457 12.037 5.94759 12.037H15.8219C16.3238 12.037 16.732 12.4452 16.732 12.9471V13.9669Z" fill="black"/>
<path d="M10.2759 20.161V23.0556C10.2759 24.3328 11.3149 25.3718 12.5921 25.3718H22.4664C23.7436 25.3718 24.7826 24.3328 24.7826 23.0556V20.161C24.7826 18.8838 23.7436 17.8447 22.4664 17.8447H12.5921C11.3149 17.8446 10.2759 18.8838 10.2759 20.161ZM23.3765 20.161V23.0556C23.3765 23.5575 22.9683 23.9657 22.4664 23.9657H12.5921C12.0902 23.9657 11.682 23.5575 11.682 23.0556V20.161C11.682 19.6591 12.0902 19.2508 12.5921 19.2508H22.4664C22.9683 19.2507 23.3765 19.6591 23.3765 20.161Z" fill="black"/>
<path d="M3.63135 32.1435C3.63135 33.4207 4.67037 34.4597 5.94759 34.4597H15.8219C17.0991 34.4597 18.1381 33.4207 18.1381 32.1435V29.2489C18.1381 27.9716 17.0991 26.9326 15.8219 26.9326H5.94759C4.67037 26.9326 3.63135 27.9716 3.63135 29.2489V32.1435ZM5.03746 29.2489C5.03746 28.747 5.4457 28.3387 5.94759 28.3387H15.8219C16.3238 28.3387 16.732 28.747 16.732 29.2489V32.1435C16.732 32.6454 16.3238 33.0536 15.8219 33.0536H5.94759C5.4457 33.0536 5.03746 32.6454 5.03746 32.1435V29.2489Z" fill="black"/>
<path d="M38.1102 19.9331C33.5406 19.9331 29.6852 23.0494 28.5583 27.2682V3.4397C28.5583 1.81976 27.2404 0.501953 25.6205 0.501953H2.93784C1.3179 0.501953 0 1.81986 0 3.4397V20.0432C0 20.4314 0.314688 20.7462 0.703056 20.7462C1.09142 20.7462 1.40611 20.4314 1.40611 20.0432V8.45258H27.1521V28.8182C23.2126 29.346 20.0003 32.203 18.9507 35.9537H5.77819C4.52478 35.9537 3.50497 36.9735 3.50497 38.227C3.50497 39.4805 4.52478 40.5002 5.77819 40.5002H18.7678C18.9094 41.2299 19.1315 41.9311 19.4245 42.5943H2.93784C2.09323 42.5943 1.40611 41.9072 1.40611 41.0626V23.6671C1.40611 23.2788 1.09142 22.964 0.703056 22.964C0.314688 22.964 0 23.2788 0 23.6671V41.0626C0 42.6825 1.3179 44.0004 2.93784 44.0004H20.1877C22.6379 47.9355 28.1585 49.6822 32.5122 47.6362L34.4133 48.4393C34.9706 48.5729 35.2962 48.357 35.39 47.7917V45.6731C37.0359 44.0629 38.0518 41.9709 38.2985 39.6981C39.9145 39.6673 41.5059 39.2395 42.9126 38.4563C43.7068 38.0141 43.0228 36.7855 42.2286 37.2278C39.8606 38.5633 36.8283 38.6501 34.3934 37.4377C34.2107 37.3484 33.9984 37.3426 33.8112 37.4218L32.5984 37.9342V36.5738C32.5984 36.3771 32.516 36.1895 32.3712 36.0564C26.7203 30.87 30.4316 21.3351 38.1102 21.3392C45.1057 21.3061 49.1324 29.5318 44.8031 35.021C44.2444 35.738 45.3535 36.6024 45.9122 35.8853C50.961 29.4838 46.2654 19.8946 38.1102 19.9331ZM1.40611 7.04647V3.4397C1.40611 2.59509 2.09323 1.90807 2.93784 1.90807H25.6205C26.4651 1.90807 27.1522 2.59519 27.1522 3.4397V7.04647H1.40611ZM5.77819 39.0941C4.62902 39.0477 4.6277 37.4068 5.77819 37.3598H18.6662C18.5935 37.9316 18.5709 38.5182 18.5987 39.0941H5.77819ZM31.1924 36.8756V38.9945C31.2862 39.5598 31.6118 39.7757 32.169 39.6421L34.0701 38.839C34.9717 39.2439 35.9161 39.5072 36.8914 39.6264C36.6543 41.6316 35.7171 43.4683 34.211 44.8537C34.0663 44.9869 33.9839 45.1745 33.9839 45.3712V46.7316L32.7711 46.2192C32.5838 46.1401 32.3715 46.1458 32.1888 46.2351C26.6793 49.009 19.9179 44.7879 19.9931 38.6138C19.993 34.0203 23.6664 30.2683 28.2304 30.14C28.3143 32.6748 29.3798 35.0983 31.1924 36.8756Z" fill="black"/>
<path d="M40.2727 27.1796C41.2036 27.1473 41.2028 25.8053 40.2727 25.7734H35.9479C35.017 25.8057 35.0177 27.1477 35.9479 27.1796H40.2727Z" fill="black"/>
<path d="M42.9383 29.1133H33.2819C32.351 29.1455 32.3517 30.4875 33.2819 30.5194H42.9383C43.8692 30.4871 43.8685 29.1452 42.9383 29.1133Z" fill="black"/>
<path d="M35.9479 32.4531C35.017 32.4854 35.0177 33.8274 35.9479 33.8592H40.2727C41.2036 33.827 41.2028 32.485 40.2727 32.4531H35.9479Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_6378_116">
<rect width="48" height="48" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,22 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6311_9508)">
<path d="M126.125 15.0417C127.161 15.0417 128 14.2022 128 13.1667V6.11825C128 2.74475 125.255 0 121.882 0H79.0155C75.6418 0 72.897 2.74475 72.897 6.11825V32.04C72.897 32.1707 72.8253 32.2905 72.71 32.3522L66.7345 35.552C65.5993 36.16 64.8943 37.3375 64.8943 38.625C64.8943 39.9125 65.5993 41.09 66.7345 41.698L72.71 44.8975C72.8255 44.9595 72.8973 45.079 72.8973 45.21V60.1317C72.8973 63.5055 75.642 66.25 79.0158 66.25H121.882C125.255 66.25 128 63.5052 128 60.1317V21.8333C128 20.7978 127.161 19.9583 126.125 19.9583C125.09 19.9583 124.25 20.7978 124.25 21.8333V60.1315C124.25 61.4375 123.188 62.4997 121.882 62.4997H79.0155C77.7095 62.4997 76.647 61.4373 76.647 60.1315V45.21C76.647 43.694 75.8168 42.3075 74.4803 41.5918L68.9398 38.625L74.4805 35.658C75.817 34.9422 76.6473 33.556 76.6473 32.04V6.11825C76.6473 4.81225 77.7098 3.75 79.0158 3.75H121.882C123.188 3.75 124.25 4.8125 124.25 6.11825V13.1665C124.25 14.2023 125.09 15.0417 126.125 15.0417Z" fill="black"/>
<path d="M45 49.6535V46.1367C47.5645 45.614 49.5 43.3415 49.5 40.625C49.5 37.5233 46.9767 35 43.875 35H34.875C31.7733 35 29.25 37.5233 29.25 40.625C29.25 43.3415 31.1855 45.6142 33.75 46.1367V49.6535C14.6938 52.3915 0 68.8232 0 88.625C0 100.683 5.4325 111.92 14.9047 119.453C15.7153 120.098 16.8947 119.964 17.5392 119.153C18.184 118.342 18.0495 117.163 17.239 116.519C8.6665 109.701 3.75 99.5338 3.75 88.625C3.75 68.9813 19.7313 53 39.375 53C59.0187 53 75 68.9813 75 88.625C75 108.269 59.0187 124.25 39.375 124.25C34.1278 124.25 29.0715 123.131 24.3463 120.923C23.408 120.485 22.2922 120.89 21.8538 121.828C21.4153 122.766 21.8205 123.882 22.7588 124.32C27.9848 126.762 33.5752 128 39.375 128C61.0865 128 78.75 110.337 78.75 88.625C78.75 68.8232 64.0563 52.3915 45 49.6535ZM33 40.625C33 39.591 33.841 38.75 34.875 38.75H43.875C44.909 38.75 45.75 39.591 45.75 40.625C45.75 41.659 44.909 42.5 43.875 42.5H34.875C33.841 42.5 33 41.659 33 40.625ZM37.5 49.2957V46.25H41.25V49.2957C40.6285 49.2665 40.0037 49.25 39.375 49.25C38.7463 49.25 38.1215 49.2665 37.5 49.2957Z" fill="black"/>
<path d="M71.25 88.625C71.25 71.049 56.951 56.75 39.375 56.75C21.799 56.75 7.5 71.049 7.5 88.625C7.5 106.201 21.799 120.5 39.375 120.5C56.951 120.5 71.25 106.201 71.25 88.625ZM11.25 88.625C11.25 73.1167 23.8667 60.5 39.375 60.5C54.8833 60.5 67.5 73.1167 67.5 88.625C67.5 104.133 54.8833 116.75 39.375 116.75C23.8667 116.75 11.25 104.133 11.25 88.625Z" fill="black"/>
<path d="M114.771 35.4187C116.496 33.7365 117.106 31.268 116.361 28.9762C115.616 26.6845 113.672 25.0457 111.288 24.6992L108.759 24.3317C108.149 24.243 107.622 23.86 107.349 23.3072L106.219 21.0152C105.152 18.8542 102.993 17.5117 100.583 17.5117C98.1738 17.5117 96.0145 18.8542 94.9483 21.015L93.8175 23.3067C93.5448 23.8597 93.0175 24.243 92.4073 24.3317L89.879 24.6992C87.4945 25.0457 85.5508 26.6845 84.806 28.9762C84.0613 31.268 84.6705 33.7365 86.3958 35.4187L88.226 37.2032C88.6678 37.634 88.869 38.254 88.7648 38.862L88.3325 41.3825C87.9253 43.7575 88.8835 46.1122 90.8328 47.5285C91.9343 48.3287 93.2228 48.7352 94.52 48.7352C95.519 48.7352 96.523 48.4942 97.4508 48.0062L99.7123 46.817C100.258 46.5302 100.909 46.53 101.455 46.817L103.715 48.0055C105.848 49.1267 108.384 48.944 110.334 47.5277C112.284 46.1115 113.242 43.7562 112.834 41.381L112.402 38.862C112.298 38.254 112.5 37.634 112.941 37.2035L114.771 35.4187ZM112.153 32.7337L110.323 34.5182C108.998 35.8105 108.393 37.6715 108.706 39.4957L109.138 42.0147C109.305 42.9872 108.928 43.914 108.13 44.4937C107.332 45.0737 106.334 45.1457 105.46 44.6865L103.2 43.498C101.562 42.6365 99.6053 42.6367 97.9668 43.498L95.7053 44.6872C94.8323 45.1462 93.8348 45.0742 93.0368 44.4945C92.2388 43.9147 91.862 42.9885 92.0285 42.0162L92.4608 39.496C92.7738 37.6715 92.1693 35.8107 90.8438 34.5182L89.0135 32.7337C88.307 32.045 88.0673 31.0737 88.3723 30.1352C88.6773 29.197 89.442 28.5522 90.4183 28.4102L92.9465 28.0427C94.7785 27.7765 96.3613 26.6262 97.1803 24.9662L98.311 22.6745C98.7475 21.79 99.5968 21.262 100.583 21.262C101.57 21.262 102.419 21.79 102.855 22.6745L103.986 24.9665C104.805 26.6265 106.388 27.7765 108.22 28.0427L110.748 28.4102C111.725 28.5522 112.489 29.197 112.794 30.1352C113.099 31.0735 112.86 32.045 112.153 32.7337Z" fill="black"/>
<path d="M39.375 69.328C40.4105 69.328 41.25 68.4885 41.25 67.453V66.625C41.25 65.5895 40.4105 64.75 39.375 64.75C38.3395 64.75 37.5 65.5895 37.5 66.625V67.453C37.5 68.4885 38.3395 69.328 39.375 69.328Z" fill="black"/>
<path d="M41.25 110.625V109.797C41.25 108.761 40.4105 107.922 39.375 107.922C38.3395 107.922 37.5 108.761 37.5 109.797V110.625C37.5 111.66 38.3395 112.5 39.375 112.5C40.4105 112.5 41.25 111.66 41.25 110.625Z" fill="black"/>
<path d="M55.6717 74.9795L56.2572 74.394C56.9895 73.6618 56.9895 72.4745 56.2572 71.7425C55.525 71.0103 54.3377 71.0103 53.6057 71.7425L53.0202 72.328C52.288 73.0603 52.2625 74.2738 53.0202 74.9795C53.802 75.708 55.0207 75.6248 55.6717 74.9795Z" fill="black"/>
<path d="M23.0783 102.27L22.4928 102.855C21.7605 103.588 21.7605 104.775 22.4928 105.507C23.203 106.217 24.4155 106.236 25.1443 105.507L25.7298 104.921C26.462 104.189 26.462 103.002 25.7298 102.27C24.9978 101.538 23.8105 101.538 23.0783 102.27Z" fill="black"/>
<path d="M60.5471 90.5H61.3751C62.4106 90.5 63.2501 89.6605 63.2501 88.625C63.2501 87.5895 62.4106 86.75 61.3751 86.75H60.5471C59.5116 86.75 58.6721 87.5895 58.6721 88.625C58.6721 89.6605 59.5116 90.5 60.5471 90.5Z" fill="black"/>
<path d="M18.203 86.75H17.375C16.3395 86.75 15.5 87.5895 15.5 88.625C15.5 89.6605 16.3395 90.5 17.375 90.5H18.203C19.2385 90.5 20.078 89.6605 20.078 88.625C20.078 87.5895 19.2385 86.75 18.203 86.75Z" fill="black"/>
<path d="M53.6054 105.507C54.3159 106.217 55.5276 106.236 56.2569 105.507C56.9891 104.775 56.9891 103.587 56.2569 102.855L55.6714 102.27C54.9391 101.538 53.7519 101.538 53.0199 102.27C52.2876 103.002 52.2876 104.189 53.0199 104.921L53.6054 105.507Z" fill="black"/>
<path d="M25.1445 71.7425C24.4123 71.0103 23.225 71.0103 22.493 71.7425C21.7608 72.4748 21.7608 73.662 22.493 74.394C22.493 74.394 23.0778 74.9788 23.0785 74.9795C23.784 75.685 25.0075 75.702 25.73 74.9795C26.4623 74.2473 26.4623 73.06 25.73 72.328L25.1445 71.7425Z" fill="black"/>
<path d="M42.5417 93.2714L49.2532 98.3992C49.593 98.6587 49.9932 98.7844 50.3902 98.7844C50.9545 98.7844 51.5125 98.5307 51.8815 98.0477C52.5102 97.2247 52.3527 96.0482 51.53 95.4194L44.7607 90.2474C44.9157 89.7337 45.0002 89.1892 45.0002 88.6254C45.0002 86.1809 43.4325 84.0969 41.2502 83.3229V74.9092C41.2502 73.8737 40.4107 73.0342 39.3752 73.0342C38.3397 73.0342 37.5002 73.8737 37.5002 74.9092V83.3229C35.318 84.0969 33.7502 86.1809 33.7502 88.6254C33.7502 91.7272 36.2735 94.2504 39.3752 94.2504C40.5487 94.2504 41.6392 93.8887 42.5417 93.2714ZM37.5 88.6254C37.5 87.5914 38.341 86.7504 39.375 86.7504C40.409 86.7504 41.25 87.5914 41.25 88.6254C41.25 89.6594 40.409 90.5004 39.375 90.5004C38.341 90.5004 37.5 89.6594 37.5 88.6254Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_6311_9508">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,10 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M117.375 5.82422H27.1625C26.127 5.82422 25.2875 6.66347 25.2875 7.69922C25.2875 8.73497 26.127 9.57422 27.1625 9.57422H117.375C121.166 9.57422 124.25 12.6582 124.25 16.4492V29.1212H3.75V16.4492C3.75 12.6585 6.834 9.57422 10.625 9.57422H18.4125C19.448 9.57422 20.2875 8.73497 20.2875 7.69922C20.2875 6.66347 19.448 5.82422 18.4125 5.82422H10.625C4.76625 5.82422 0 10.5905 0 16.4492V107.668C0 113.527 4.76625 118.293 10.625 118.293H48.4953C53.1185 120.77 58.3983 122.176 64 122.176C69.6018 122.176 74.8815 120.77 79.5048 118.293H117.375C123.234 118.293 128 113.527 128 107.668V16.4492C128 10.5905 123.234 5.82422 117.375 5.82422ZM37.2557 77.5242H47.687C47.0868 80.5977 46.7183 83.9017 46.6203 87.3632H34.8717C35.0877 83.9435 35.8947 80.6245 37.2557 77.5242ZM73.674 116.778C76.1003 113.758 78.0847 109.612 79.4355 104.702H88.7452C85.26 110.259 79.956 114.565 73.674 116.778ZM73.3053 68.1122C74.1665 69.8347 74.909 71.7352 75.5245 73.774H65.875V60.3527C68.5888 61.2235 71.2215 63.945 73.3053 68.1122ZM76.4805 77.5242C77.1335 80.613 77.5238 83.9302 77.628 87.3632H65.875V77.5242H76.4805ZM77.6277 91.1132C77.5235 94.5462 77.1332 97.8635 76.4802 100.952H65.875V91.1132H77.6277ZM75.5242 104.702C74.9087 106.741 74.1663 108.642 73.305 110.364C71.2215 114.531 68.5887 117.253 65.8747 118.124V104.702H75.5242ZM62.125 100.952H51.5195C50.8665 97.8637 50.4763 94.5465 50.372 91.1135H62.125V100.952ZM62.125 104.702V118.124C59.4113 117.253 56.7785 114.531 54.6948 110.364C53.8335 108.642 53.0913 106.741 52.4757 104.702H62.125ZM90.733 100.952H80.313C80.9133 97.879 81.2818 94.575 81.3798 91.1135H93.128C92.9062 94.5922 92.0723 97.9077 90.733 100.952ZM81.3798 87.3632C81.2818 83.9015 80.9135 80.5977 80.313 77.5242H90.733C92.0725 80.569 92.9062 83.8842 93.128 87.3632H81.3798ZM79.4355 73.7742C78.0847 68.8642 76.1003 64.7182 73.674 61.6985C79.956 63.9117 85.26 68.2172 88.7452 73.7742H79.4355ZM62.125 60.353V73.7742H52.4757C53.0913 71.7355 53.8338 69.8347 54.6948 68.1125C56.7785 63.945 59.4113 61.2235 62.125 60.353ZM62.125 77.5242V87.3632H50.3722C50.4765 83.9302 50.8667 80.613 51.5197 77.5242H62.125ZM46.6203 91.1132C46.7183 94.575 47.0865 97.8787 47.687 100.952H37.267C35.9277 97.9075 35.0938 94.5922 34.872 91.1132H46.6203ZM48.5645 104.702C49.9152 109.612 51.8997 113.758 54.326 116.778C48.044 114.565 42.74 110.259 39.2547 104.702H48.5645ZM124.25 107.668C124.25 111.459 121.166 114.543 117.375 114.543H85.0622C92.3148 108.496 96.9375 99.397 96.9375 89.2382C96.9375 71.1897 82.282 56.3007 64 56.3007C56.5457 56.3007 49.2417 58.8607 43.4333 63.5095C42.6248 64.1565 42.494 65.3365 43.141 66.145C43.7878 66.9532 44.968 67.0847 45.7765 66.4372C48.3908 64.345 51.276 62.7467 54.347 61.6722C51.9113 64.694 49.9193 68.8497 48.5645 73.774H39.2405C39.506 73.35 39.783 72.932 40.0715 72.5197C40.6652 71.6712 40.4592 70.5022 39.6107 69.9085C38.7625 69.3142 37.5933 69.5202 36.9995 70.369C33.1152 75.917 31.0623 82.442 31.0623 89.2382C31.0623 99.397 35.6852 108.496 42.9375 114.543H10.625C6.834 114.543 3.75 111.459 3.75 107.668V48.4022H78.2968C79.3325 48.4022 80.1718 47.563 80.1718 46.5272C80.1718 45.4915 79.3325 44.6522 78.2968 44.6522H3.75V32.871H124.25V36.8865H104.122C102.787 36.8865 101.531 37.4065 100.587 38.3507L94.6523 44.2855C94.4157 44.5217 94.1017 44.652 93.768 44.652H87.2968C86.261 44.652 85.4218 45.4912 85.4218 46.527C85.4218 47.5627 86.261 48.402 87.2968 48.402H93.768C95.1032 48.402 96.3585 47.882 97.3037 46.9377L103.239 41.0027C103.475 40.7667 103.788 40.6365 104.122 40.6365H124.25V107.668Z" fill="black"/>
<path d="M120.234 19.3476C120.234 16.1728 117.651 13.5898 114.477 13.5898C111.302 13.5898 108.719 16.1728 108.719 19.3476C108.719 22.5223 111.302 25.1053 114.477 25.1053C117.651 25.1053 120.234 22.5223 120.234 19.3476ZM112.469 19.3476C112.469 18.2403 113.369 17.3398 114.476 17.3398C115.583 17.3398 116.484 18.2406 116.484 19.3476C116.484 20.4546 115.583 21.3553 114.476 21.3553C113.369 21.3553 112.469 20.4546 112.469 19.3476Z" fill="black"/>
<path d="M104.703 19.3476C104.703 16.1728 102.12 13.5898 98.9453 13.5898C95.7705 13.5898 93.1875 16.1726 93.1875 19.3476C93.1875 22.5226 95.7705 25.1053 98.9453 25.1053C102.12 25.1053 104.703 22.5223 104.703 19.3476ZM96.9375 19.3476C96.9375 18.2403 97.8383 17.3398 98.9453 17.3398C100.052 17.3398 100.953 18.2406 100.953 19.3476C100.953 20.4546 100.052 21.3553 98.9453 21.3553C97.8383 21.3553 96.9375 20.4546 96.9375 19.3476Z" fill="black"/>
<path d="M89.172 19.3476C89.172 16.1728 86.589 13.5898 83.4142 13.5898C80.2395 13.5898 77.6565 16.1728 77.6565 19.3476C77.6565 22.5223 80.2395 25.1053 83.4142 25.1053C86.589 25.1053 89.172 22.5223 89.172 19.3476ZM81.4062 19.3476C81.4062 18.2403 82.307 17.3398 83.414 17.3398C84.521 17.3398 85.4217 18.2406 85.4217 19.3476C85.4217 20.4546 84.521 21.3553 83.414 21.3553C82.307 21.3553 81.4062 20.4546 81.4062 19.3476Z" fill="black"/>
<path d="M7.76538 17.3398V21.3553C7.76538 23.4231 9.44763 25.1053 11.5154 25.1053H38.8279C40.8956 25.1053 42.5779 23.4231 42.5779 21.3553V17.3398C42.5779 15.2721 40.8956 13.5898 38.8279 13.5898H11.5154C9.44788 13.5898 7.76538 15.2721 7.76538 17.3398ZM38.8304 21.3556H11.5154V17.3398H38.8279L38.8304 21.3556Z" fill="black"/>
<path d="M93.0546 38.7617C93.0546 37.726 92.2154 36.8867 91.1796 36.8867H25.1716C24.1361 36.8867 23.2966 37.726 23.2966 38.7617C23.2966 39.7975 24.1361 40.6367 25.1716 40.6367H91.1794C92.2151 40.6367 93.0546 39.7972 93.0546 38.7617Z" fill="black"/>
<path d="M17.4062 40.6367C18.4418 40.6367 19.2812 39.7973 19.2812 38.7617C19.2812 37.7262 18.4418 36.8867 17.4062 36.8867C16.3707 36.8867 15.5312 37.7262 15.5312 38.7617C15.5312 39.7973 16.3707 40.6367 17.4062 40.6367Z" fill="black"/>
<path d="M9.64038 40.6367C10.6759 40.6367 11.5154 39.7973 11.5154 38.7617C11.5154 37.7262 10.6759 36.8867 9.64038 36.8867C8.60485 36.8867 7.76538 37.7262 7.76538 38.7617C7.76538 39.7973 8.60485 40.6367 9.64038 40.6367Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 292.1 39.47"><path fill="#fff" d="M7.44 6.89h17.2v22.06H7.44z"/><path fill="#112e51" d="M53.45 5.28h-6.16v27.31h17.87v-5.38H53.45V5.28zm30.76-.19c-8.35 0-14.67 5.89-14.67 13.85s6.32 13.93 14.67 13.93 14.67-6 14.67-13.93S92.56 5.09 84.21 5.09zm.08 22.39a8.49 8.49 0 0 1-8.43-8.54 8.4 8.4 0 0 1 8.43-8.47 8.29 8.29 0 0 1 8.27 8.47 8.36 8.36 0 0 1-8.27 8.54zm35.36-17.05a11.43 11.43 0 0 1 7.49 3l3.47-4.41a17.21 17.21 0 0 0-11-4c-8.51 0-14.9 6-14.9 13.85s6.29 13.99 14.58 13.99a20.21 20.21 0 0 0 11.08-3.55V17.57h-10.11v4.82H125V26a12.42 12.42 0 0 1-5.34 1.48 8.52 8.52 0 1 1 0-17zm19.61-5.15h6.17v27.31h-6.17zm34.53 17.56L160.52 5.28h-5.61v27.31h5.89V15.07l13.22 17.52h5.62V5.28h-5.85v17.56zm17.47 2.85a3.39 3.39 0 0 0-3.47 3.56 3.47 3.47 0 1 0 6.94 0 3.36 3.36 0 0 0-3.47-3.56zm23.15-15.26a11.43 11.43 0 0 1 7.49 3l3.47-4.41a17.21 17.21 0 0 0-11-4c-8.51 0-14.9 6-14.9 13.85s6.28 13.93 14.55 13.93a20.21 20.21 0 0 0 11.08-3.55V17.57H215v4.82h4.77V26a12.42 12.42 0 0 1-5.34 1.48 8.52 8.52 0 1 1 0-17zm32.23-5.34C238.29 5.09 232 11 232 18.94s6.32 13.93 14.67 13.93 14.67-6 14.67-13.93S255 5.09 246.64 5.09zm.08 22.39a8.49 8.49 0 0 1-8.43-8.54 8.4 8.4 0 0 1 8.43-8.47 8.29 8.29 0 0 1 8.28 8.47 8.36 8.36 0 0 1-8.28 8.54zm39.02-22.2-7.49 20.6-7.45-20.6h-6.63l10.73 27.31h6.36L292.1 5.28h-6.36z"/><path fill="#e21d3e" d="m11.38 25.34 1.45-8.28a4.39 4.39 0 0 1 2.71-7.86V0H4.34A4.34 4.34 0 0 0 0 4.34v21.28a2.62 2.62 0 0 0 .26 1.16c.94 1.9 4.65 8 15.27 12.69V25.7a24.3 24.3 0 0 1-4.15-.36z"/><path fill="#b51e23" d="M26.72 0H15.53v9.19A4.39 4.39 0 0 1 18.28 17l1.45 8.32a24.29 24.29 0 0 1-4.2.36v13.79C26.14 34.8 29.86 28.68 30.8 26.79a2.62 2.62 0 0 0 .26-1.16V4.34A4.34 4.34 0 0 0 26.72 0z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,17 +0,0 @@
<svg width="305" height="304" viewBox="0 0 381 380" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M163.41 47.1289H32.7852C14.983 47.1289 0.5 61.6119 0.5 79.414V293.793C0.755313 301.164 11.3797 301.158 11.6328 293.793V79.414C11.6328 67.7506 21.1217 58.2617 32.7852 58.2617H163.41C175.074 58.2617 184.562 67.7506 184.562 79.414V335.84H11.6328V319.851C11.3775 312.481 0.753086 312.487 0.5 319.851V346.601C0.5 364.403 14.983 378.887 32.7852 378.887H163.41C181.212 378.887 195.695 364.403 195.695 346.601V79.414C195.695 61.6119 181.212 47.1289 163.41 47.1289ZM163.41 367.754H32.7852C21.2464 367.754 11.8414 358.464 11.6425 346.972H184.554C184.354 358.464 174.949 367.754 163.41 367.754Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M72.8629 102.793H123.332C145.461 101.951 145.444 70.2287 123.332 69.3945H72.8629C50.7338 70.2362 50.7509 101.959 72.8629 102.793ZM72.8629 80.5273H123.332C130.701 80.7893 130.696 91.4004 123.332 91.6601H72.8629C65.4937 91.3981 65.4989 80.7871 72.8629 80.5273Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M41.6899 265.26V303.853C41.6899 308.56 45.5189 312.389 50.2251 312.389H145.225C149.931 312.389 153.76 308.56 153.76 303.853V265.26C153.76 260.554 149.931 256.725 145.225 256.725H50.2251C45.5181 256.725 41.6899 260.554 41.6899 265.26ZM52.8228 267.857H142.627V301.256H52.8228V267.857Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M374.934 50.0976C378.008 50.0976 380.5 47.6054 380.5 44.5312V21.4203C380.5 9.60909 370.835 0 358.955 0H258.626C246.746 0 237.081 9.60909 237.081 21.4203V99.174L211.672 116.104C207.446 118.746 207.447 125.434 211.673 128.076L237.081 145.005V195.298C237.081 207.109 246.746 216.718 258.626 216.718H358.955C370.835 216.719 380.5 207.109 380.5 195.298V71.9098C380.245 64.5391 369.62 64.545 369.367 71.9098V195.298C369.367 200.971 364.697 205.586 358.955 205.586H258.626C252.885 205.586 248.214 200.971 248.214 195.298V143.773C248.214 140.812 246.74 138.063 244.272 136.418L222.766 122.09L244.272 107.761C246.741 106.116 248.214 103.367 248.214 100.407V21.4203C248.214 15.7477 252.885 11.1328 258.626 11.1328H358.955C364.697 11.1328 369.367 15.7477 369.367 21.4203V44.5312C369.367 47.6054 371.86 50.0976 374.934 50.0976Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M279.025 44.4072H313.172C320.542 44.1519 320.537 33.5275 313.172 33.2744H279.025C271.655 33.5305 271.66 44.1549 279.025 44.4072Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M279.025 116.647H313.172C320.542 116.392 320.537 105.768 313.172 105.515H279.025C271.655 105.77 271.66 116.394 279.025 116.647Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M279.025 62.4678H336.689C344.059 62.2125 344.054 51.588 336.689 51.335H279.025C271.655 51.5903 271.66 62.2147 279.025 62.4678Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M279.025 80.5273H336.689C344.059 80.272 344.054 69.6476 336.689 69.3945H279.025C271.655 69.6498 271.66 80.2743 279.025 80.5273Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M98.0967 205.319C108.123 205.319 116.28 197.161 116.28 187.135C115.366 163.045 80.8237 163.052 79.9131 187.135C79.9131 197.161 88.0705 205.319 98.0967 205.319ZM98.0967 180.084C101.984 180.084 105.147 183.248 105.147 187.135C104.793 196.476 91.3992 196.473 91.0459 187.135C91.0459 183.247 94.2091 180.084 98.0967 180.084Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M49.0186 181.389C49.7927 183.69 51.1932 185.667 53.0569 187.135C51.1932 188.603 49.7935 190.58 49.0186 192.88C47.9662 196.005 48.1948 199.354 49.6621 202.308L56.7158 216.509C59.7425 222.603 67.1644 225.101 73.2592 222.078L77.814 219.82V224.885C77.814 231.693 83.3529 237.233 90.1618 237.233H106.039C112.844 237.233 118.38 231.697 118.38 224.891V219.82L122.935 222.078C129.032 225.101 136.452 222.603 139.479 216.509L146.533 202.305C148 199.352 148.229 196.004 147.177 192.879C146.403 190.578 145.001 188.601 143.138 187.134C147.636 183.694 149.157 177.013 146.532 171.96L139.48 157.763C136.453 151.668 129.029 149.168 122.932 152.192L118.38 154.449V149.378C118.38 142.573 112.844 137.036 106.039 137.036H90.1618C83.3537 137.036 77.814 142.575 77.814 149.384V154.449L73.2622 152.192C67.1651 149.168 59.7417 151.667 56.7144 157.762L49.6621 171.96C48.1948 174.916 47.9662 178.264 49.0186 181.389ZM59.6319 176.914L66.6842 162.717C66.9825 162.115 67.7151 161.867 68.3162 162.167L78.764 167.347C80.9646 168.437 83.5259 168.316 85.6129 167.021C87.7007 165.726 88.9468 163.486 88.9468 161.03V149.385C88.9468 148.715 89.4916 148.17 90.1618 148.17H106.039C106.705 148.17 107.247 148.712 107.247 149.379V161.03C107.247 163.486 108.493 165.726 110.581 167.021C112.668 168.316 115.228 168.439 117.43 167.346L127.878 162.166C128.48 161.868 129.211 162.115 129.51 162.716L136.563 176.913C136.747 177.284 136.698 177.62 136.626 177.836C136.553 178.053 136.388 178.349 136.016 178.533L131.411 180.817C126.23 183.267 126.231 191.002 131.411 193.451L136.019 195.736C136.39 195.92 136.554 196.215 136.626 196.431C136.699 196.647 136.747 196.982 136.563 197.351L129.509 211.555C129.211 212.154 128.48 212.401 127.881 212.103L117.43 206.921C115.229 205.83 112.668 205.952 110.582 207.247C108.494 208.542 107.248 210.782 107.248 213.239V224.89C107.248 225.556 106.706 226.099 106.039 226.099H90.1625C89.4931 226.099 88.9476 225.554 88.9476 224.884V213.239C88.9476 210.782 87.7014 208.542 85.6137 207.247C83.5229 205.95 80.9601 205.832 78.764 206.922L68.3133 212.104C67.7121 212.401 66.9833 212.155 66.6864 211.556L59.6326 197.356C59.4486 196.985 59.4968 196.649 59.5696 196.432C59.6423 196.215 59.807 195.919 60.1781 195.735L64.7842 193.451C69.9654 191.001 69.9639 183.266 64.7842 180.818L60.1789 178.534C59.8078 178.35 59.643 178.053 59.5703 177.837C59.4968 177.62 59.4486 177.285 59.6319 176.914Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M363.426 257.783H235.796C226.382 257.783 218.723 265.442 218.723 274.856V362.927C218.723 372.341 226.383 380 235.796 380H363.426C372.841 380 380.499 372.341 380.499 362.927V274.856C380.499 265.442 372.841 257.783 363.426 257.783ZM369.366 362.927C369.366 366.203 366.702 368.867 363.426 368.867H235.796C232.52 368.867 229.856 366.203 229.856 362.927V274.856C229.856 271.58 232.521 268.916 235.796 268.916H363.426C366.702 268.916 369.366 271.58 369.366 274.856V362.927Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M268.272 141.779V181.381C268.272 185.809 271.875 189.412 276.304 189.412H340.926C345.354 189.412 348.956 185.809 348.956 181.381V141.779C348.956 137.351 345.354 133.749 340.926 133.749H276.304C271.875 133.748 268.272 137.35 268.272 141.779ZM279.405 144.881H337.824V178.279H279.405V144.881Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M273.174 297.591C271.206 295.229 267.695 294.909 265.334 296.878L241.975 316.344C239.33 318.441 239.331 322.8 241.975 324.897L265.334 344.364C266.375 345.23 267.638 345.654 268.895 345.653C270.489 345.653 272.073 344.971 273.174 343.65C275.142 341.289 274.823 337.779 272.462 335.811L254.233 320.62L272.462 305.431C274.823 303.462 275.142 299.953 273.174 297.591Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M357.249 316.344L333.889 296.878C331.527 294.91 328.018 295.229 326.049 297.59C324.081 299.952 324.4 303.462 326.762 305.43L344.99 320.62L326.762 335.81C324.4 337.778 324.081 341.288 326.049 343.65C328.026 346.021 331.541 346.322 333.889 344.362L357.249 324.895C359.893 322.799 359.892 318.441 357.249 316.344Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
<path d="M314.73 284.336C311.892 283.154 308.634 284.495 307.451 287.333L281.496 349.626C280.314 352.464 281.656 355.723 284.493 356.905C287.26 358.079 290.59 356.775 291.772 353.908L317.727 291.616C318.91 288.778 317.569 285.519 314.73 284.336Z" fill="#005EA2" stroke="#DFE1E2" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -1,3 +0,0 @@
<svg viewBox="0 0 563 670" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M553.831 334.751V335.974L555.03 335.731C555.104 335.716 555.181 335.708 555.262 335.708H557.846C558.483 335.708 559 336.224 559 336.862V418.708C559 419.345 558.483 419.862 557.846 419.862H555.262C555.181 419.862 555.104 419.853 555.03 419.838L553.831 419.595V420.818V955.446C553.831 970.12 547.528 991.015 535.312 1008.3C522.978 1025.76 503.794 1040.6 477.723 1040.6H82.2769C56.206 1040.6 37.022 1025.76 24.6885 1008.3C12.472 991.015 6.16923 970.12 6.16923 955.446V420.818V419.595L4.97026 419.838C4.89621 419.853 4.8188 419.862 4.73846 419.862H2.15385C1.51659 419.862 1 419.345 1 418.708V358.4C1 357.763 1.51659 357.246 2.15385 357.246H4.73846C4.8188 357.246 4.89621 357.254 4.97026 357.269L6.16923 357.513V356.289V347.588V346.364L4.97026 346.608C4.89621 346.623 4.8188 346.631 4.73846 346.631H2.15385C1.51659 346.631 1 346.114 1 345.477V285.169C1 284.532 1.51659 284.015 2.15385 284.015H4.73846C4.8188 284.015 4.89621 284.023 4.97026 284.038L6.16923 284.282V283.058V261.434V260.21L4.97026 260.454C4.89621 260.469 4.8188 260.477 4.73846 260.477H2.15385C1.51659 260.477 1 259.96 1 259.323V220.554C1 219.917 1.51659 219.4 2.15385 219.4H4.73846C4.8188 219.4 4.89624 219.408 4.97036 219.423L6.16923 219.666V218.443V86.1538C6.16923 71.4804 12.472 50.5848 24.6885 33.2974C37.022 15.8443 56.206 1 82.2769 1H477.723C503.794 1 522.978 15.8443 535.312 33.2974C547.528 50.5848 553.831 71.4804 553.831 86.1538V334.751ZM400.615 16.2308H399.615V17.2308V43.0769C399.615 49.6619 394.277 55 387.692 55H172.308C165.723 55 160.385 49.6619 160.385 43.0769V17.2308V16.2308H159.385H82.2769C62.6883 16.2308 47.6257 27.2304 37.1269 42.0872C26.5125 57.1076 21.4 74.9813 21.4 86.1538V955.446C21.4 966.619 26.5125 984.492 37.1269 999.513C47.6257 1014.37 62.6883 1025.37 82.2769 1025.37H477.723C497.312 1025.37 512.374 1014.37 522.873 999.513C533.487 984.492 538.6 966.619 538.6 955.446V86.1538C538.6 74.9813 533.488 57.1076 522.873 42.0872C512.374 27.2304 497.312 16.2308 477.723 16.2308H400.615Z" stroke="black" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 425.2 428" style="enable-background:new 0 0 425.2 428;" xml:space="preserve">
<style type="text/css">
.st0{fill:#1D70B8;}
.st1{fill:#0B0C0C;}
.st2{fill:#FFDD00;}
.st3{opacity:0.2;fill:#1D2F3A;}
.st4{opacity:0.5;fill:#F4F4F4;}
.st5{clip-path:url(#SVGID_2_);fill:#FFDD00;}
.st6{opacity:0.2;clip-path:url(#SVGID_2_);fill:#1D2F3A;}
.st7{opacity:0.5;clip-path:url(#SVGID_2_);}
.st8{fill:#F4F4F4;}
.st9{fill:#206FAB;}
</style>
<g id="Layer_1">
<circle class="st0" cx="212.6" cy="212.6" r="212.6"/>
</g>
<g id="Layer_2">
<rect x="91.9" y="248.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -164.6926 181.3616)" class="st1" width="89.3" height="81.7"/>
<rect x="76.9" y="258.5" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -150.6822 163.09)" class="st2" width="89.3" height="9.9"/>
<rect x="90.6" y="272.2" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -156.3567 176.7894)" class="st2" width="89.3" height="9.9"/>
<rect x="104.3" y="285.9" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -162.0312 190.4889)" class="st2" width="89.3" height="9.9"/>
<rect x="118" y="299.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -167.7057 204.1884)" class="st2" width="89.3" height="9.9"/>
<rect x="131.9" y="227.1" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -143.1309 190.2927)" class="st3" width="52.5" height="81.7"/>
</g>
<g id="Layer_5">
<polygon class="st4" points="116.8,275.2 108.5,283.4 103,274.9 111.3,266.7 "/>
<polygon class="st4" points="127.6,291.8 119.3,300 113.8,291.5 122.1,283.3 "/>
<polygon class="st4" points="138.3,308.4 130.1,316.7 124.6,308.2 132.8,299.9 "/>
<polygon class="st4" points="149.1,325 140.9,333.3 135.4,324.8 143.6,316.6 "/>
</g>
<g id="Layer_6">
</g>
<g id="Layer_4">
<defs>
<ellipse id="SVGID_1_" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st5" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st6" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
<path class="st5" d="M264.8,127.7c15.8,2.7,30.8,17.7,33.5,33.5s-7.9,26.4-23.7,23.7s-30.8-17.7-33.5-33.5
C238.4,135.6,249,125,264.8,127.7z"/>
<g class="st7">
<rect x="260.6" y="117.2" transform="matrix(0.842 -0.5395 0.5395 0.842 -44.6945 168.5803)" class="st8" width="9.8" height="86.7"/>
</g>
</g>
<g id="Layer_9">
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st9" cx="278.7" cy="147.3" rx="15.8" ry="15.8"/>
</g>
<g id="Layer_3">
<path class="st1" d="M278,74.5l-168,168l73.6,73.6l168-168L278,74.5z M262,164c-9.2-9.2-9.2-24.2,0-33.5s24.2-9.2,33.5,0
s9.2,24.2,0,33.5S271.3,173.3,262,164z"/>
</g>
<g id="Layer_8">
</g>
<g id="Layer_10">
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,17 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6311_9575)">
<path d="M13.1356 105.339H93.2878C94.3236 105.339 95.1628 104.5 95.1628 103.464C95.1628 102.428 94.3236 101.589 93.2878 101.589H13.1356C11.6753 101.589 10.4873 100.401 10.4873 98.9407V26.0474H44.2016C41.9081 29.6852 40.5788 33.9892 40.5788 38.5982V46.6989C37.1923 47.1732 34.5773 50.0874 34.5773 53.6027V86.9362C34.5773 90.7812 37.7053 93.9092 41.5503 93.9092H86.8113C90.6563 93.9092 93.7843 90.7812 93.7843 86.9362V53.6029C93.7843 50.0877 91.1693 47.1734 87.7826 46.6992V38.5984C87.7826 33.9892 86.4533 29.6854 84.1598 26.0477H119.138V98.9409C119.138 100.401 117.95 101.589 116.49 101.589H102.282C101.246 101.589 100.407 102.428 100.407 103.464C100.407 104.5 101.246 105.339 102.282 105.339H116.49C120.018 105.339 122.888 102.469 122.888 98.9409V14.1582C122.888 9.20744 118.86 5.17969 113.909 5.17969H38.9261C37.8903 5.17969 37.0511 6.01894 37.0511 7.05469C37.0511 8.09044 37.8903 8.92969 38.9261 8.92969H113.909C116.792 8.92969 119.138 11.2752 119.138 14.1582V22.2974H81.3611C81.3201 22.2974 81.2798 22.3009 81.2393 22.3034C76.9391 17.8034 70.8815 14.9962 64.1805 14.9962C57.4533 14.9962 51.3748 17.8257 47.0716 22.3564C46.9241 22.3192 10.4873 22.2972 10.4873 22.2972V14.1582C10.4873 11.2752 12.8328 8.92969 15.7158 8.92969H30.1583C31.1941 8.92969 32.0333 8.09044 32.0333 7.05469C32.0333 6.01894 31.1941 5.17969 30.1583 5.17969H15.7158C10.7651 5.17969 6.7373 9.20744 6.7373 14.1582V98.9409C6.7373 102.469 9.60755 105.339 13.1356 105.339ZM90.0343 53.6029V86.9364C90.0343 88.7134 88.5886 90.1594 86.8113 90.1594H41.5503C39.7733 90.1594 38.3273 88.7137 38.3273 86.9364V53.6029C38.3273 51.8259 39.7731 50.3799 41.5503 50.3799H86.8113C88.5886 50.3802 90.0343 51.8259 90.0343 53.6029ZM74.5733 46.6302H53.7881V38.5984C53.7881 32.8677 58.4501 28.2057 64.1805 28.2057C69.911 28.2057 74.5731 32.8677 74.5731 38.5984V46.6302H74.5733ZM64.1808 18.7464C75.1271 18.7464 84.0326 27.6519 84.0326 38.5982V46.6299H78.3233V38.5982C78.3233 30.7999 71.979 24.4554 64.1808 24.4554C56.3826 24.4554 50.0383 30.7999 50.0383 38.5982V46.6299H44.3291V38.5982C44.3288 27.6519 53.2346 18.7464 64.1808 18.7464Z" fill="black"/>
<path d="M91.6201 17.4883C92.6558 17.4883 93.4951 16.649 93.4951 15.6133C93.4951 14.5775 92.6558 13.7383 91.6201 13.7383H89.5701C88.5343 13.7383 87.6951 14.5775 87.6951 15.6133C87.6951 16.649 88.5343 17.4883 89.5701 17.4883H91.6201Z" fill="black"/>
<path d="M100.641 17.4883C101.676 17.4883 102.516 16.649 102.516 15.6133C102.516 14.5775 101.676 13.7383 100.641 13.7383H98.5906C97.5548 13.7383 96.7156 14.5775 96.7156 15.6133C96.7156 16.649 97.5548 17.4883 98.5906 17.4883H100.641Z" fill="black"/>
<path d="M109.661 17.4883C110.697 17.4883 111.536 16.649 111.536 15.6133C111.536 14.5775 110.697 13.7383 109.661 13.7383H107.611C106.575 13.7383 105.736 14.5775 105.736 15.6133C105.736 16.649 106.575 17.4883 107.611 17.4883H109.661Z" fill="black"/>
<path d="M126.125 113.899H105.969C105.146 110.921 102.414 108.728 99.1775 108.728C95.9412 108.728 93.2095 110.921 92.3863 113.899H35.6138C34.7905 110.921 32.0588 108.728 28.8228 108.728C25.5865 108.728 22.8547 110.921 22.0315 113.899H1.875C0.83925 113.899 0 114.738 0 115.774C0 116.81 0.83925 117.649 1.875 117.649H22.0315C22.8547 120.627 25.5865 122.82 28.8228 122.82C32.0588 122.82 34.7905 120.627 35.6138 117.649H92.3865C93.2097 120.627 95.9415 122.82 99.1777 122.82C102.414 122.82 105.146 120.627 105.969 117.649H126.125C127.161 117.649 128 116.81 128 115.774C128 114.738 127.161 113.899 126.125 113.899ZM28.8228 119.07C27.005 119.07 25.5263 117.591 25.5263 115.774C25.5263 113.956 27.005 112.477 28.8228 112.477C30.6402 112.477 32.119 113.956 32.119 115.774C32.119 117.591 30.6402 119.07 28.8228 119.07ZM99.1775 119.07C97.3597 119.07 95.881 117.591 95.881 115.774C95.881 113.956 97.3597 112.477 99.1775 112.477C100.995 112.477 102.474 113.956 102.474 115.774C102.474 117.591 100.995 119.07 99.1775 119.07Z" fill="black"/>
<path d="M74.6482 63.1457C73.9162 62.4137 72.7289 62.4135 71.9967 63.146C71.2644 63.8782 71.2644 65.0655 71.9969 65.7975L76.4694 70.2697L71.9967 74.7422C71.2644 75.4747 71.2644 76.6617 71.9967 77.394C72.3629 77.76 72.8429 77.943 73.3227 77.943C73.8024 77.943 74.2824 77.76 74.6487 77.394L80.4469 71.5957C80.7984 71.244 80.9959 70.7672 80.9959 70.2697C80.9959 69.7722 80.7984 69.2957 80.4467 68.9437L74.6482 63.1457Z" fill="black"/>
<path d="M51.8922 70.2697L56.3647 65.7974C57.0972 65.0652 57.0972 63.8779 56.365 63.1459C55.6327 62.4139 54.4455 62.4134 53.7135 63.1456L47.9152 68.9436C47.5635 69.2954 47.366 69.7722 47.366 70.2697C47.366 70.7672 47.5635 71.2436 47.915 71.5956L53.7132 77.3939C54.0795 77.7599 54.5595 77.9429 55.0392 77.9429C55.519 77.9429 55.999 77.7599 56.3652 77.3939C57.0975 76.6614 57.0975 75.4744 56.3652 74.7421L51.8922 70.2697Z" fill="black"/>
<path d="M66.0639 62.6508C65.0587 62.4026 64.0422 63.0161 63.7937 64.0211L60.9274 75.6176C60.6792 76.6228 61.2927 77.6393 62.2977 77.8878C62.4487 77.9251 62.6002 77.9431 62.7492 77.9431C63.5912 77.9431 64.3569 77.3716 64.5679 76.5176L67.4342 64.9211C67.6824 63.9158 67.0689 62.8996 66.0639 62.6508Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_6311_9575">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,12 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6311_9539)">
<path d="M107.26 81.0871C108.302 81.0871 109.145 80.2431 109.145 79.2021V61.7874C109.145 60.7464 108.302 59.9023 107.26 59.9023H103.538C102.497 59.9023 101.653 60.7464 101.653 61.7874C101.653 62.8285 102.497 63.6725 103.538 63.6725H105.375V79.2021C105.375 80.2431 106.219 81.0871 107.26 81.0871Z" fill="black"/>
<path d="M105.03 95.3332C104.018 95.0871 102.999 95.7069 102.752 96.7186L97.7374 117.306C97.6188 117.793 97.2262 117.927 97.0625 117.963C96.8997 117.998 96.486 118.037 96.1776 117.642L59.5854 70.7809L122.95 13.8064L115.318 45.1362C115.072 46.1479 115.692 47.1676 116.703 47.4139C116.853 47.4503 117.003 47.4679 117.151 47.4679C118 47.4679 118.771 46.8901 118.981 46.0285L127.914 9.35889C128.13 8.46888 127.846 7.53061 127.173 6.91004C126.499 6.28972 125.541 6.08387 124.671 6.37267L34.2959 36.438C33.3079 36.7667 32.7735 37.8339 33.1023 38.8217C33.4308 39.8095 34.4985 40.3441 35.4858 40.0153L119.097 12.2011L56.5961 68.3984L5.56399 49.9696L27.7773 42.58C28.7653 42.2513 29.2997 41.1841 28.9709 40.1963C28.6424 39.2088 27.5755 38.6744 26.5874 39.0027L1.98209 47.1877C0.793983 47.5828 0.0163241 48.647 0.000238096 49.8989C-0.0153452 51.1506 0.735168 52.2342 1.91221 52.6592L37.0421 65.3453V98.9988C37.0421 100.55 37.8471 101.934 39.1953 102.701C39.8571 103.077 40.5798 103.265 41.3014 103.265C42.0504 103.265 42.7986 103.062 43.4772 102.659L51.7857 97.7167C52.6807 97.1843 52.9743 96.0276 52.4422 95.1328C51.9098 94.2378 50.7532 93.9442 49.8584 94.4763L41.5499 99.4185C41.3479 99.5386 41.1717 99.4871 41.0591 99.4233C40.9465 99.3592 40.8122 99.2335 40.8122 98.9988V66.7068L55.8657 72.1429L65.8687 84.9529L56.7635 90.3691C55.8685 90.9015 55.5749 92.0581 56.107 92.9529C56.4597 93.5456 57.0863 93.8746 57.729 93.8746C58.057 93.8746 58.3887 93.7891 58.6908 93.6094L68.2077 87.9484L93.2062 119.962C94.1045 121.113 95.4456 121.755 96.8595 121.755C97.1892 121.755 97.523 121.72 97.856 121.648C99.6162 121.27 100.974 119.948 101.401 118.198L106.416 97.6108C106.662 96.5994 106.042 95.5797 105.03 95.3332Z" fill="black"/>
<path d="M126.484 72.7292C125.468 72.51 124.464 73.1573 124.244 74.1747C122.533 82.1121 115.391 87.873 107.26 87.873C97.6782 87.873 89.8825 80.0773 89.8825 70.495C89.8825 60.9128 97.6782 53.1171 107.26 53.1171C115.446 53.1171 122.599 58.9234 124.269 66.9232C124.482 67.9424 125.482 68.5969 126.5 68.3832C127.519 68.1703 128.173 67.1718 127.96 66.1526C125.927 56.4145 117.221 49.3467 107.261 49.3467C95.5995 49.3467 86.1125 58.8337 86.1125 70.4948C86.1125 82.1559 95.5995 91.6429 107.261 91.6429C112.123 91.6429 116.871 89.9511 120.63 86.8791C124.335 83.8517 126.927 79.6216 127.93 74.9685C128.149 73.9515 127.502 72.9484 126.484 72.7292Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_6311_9539">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><path d="m488.19 30.63h-48.55c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h48.551c4.857 0 8.81 3.952 8.81 8.811v39.835h-482.001v-39.836c0-4.858 3.952-8.811 8.81-8.811h380.83c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-380.83c-13.129.001-23.81 10.683-23.81 23.811v403.12c0 13.129 10.681 23.811 23.81 23.811h69c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-69c-4.857 0-8.81-3.952-8.81-8.811v-348.285h482v348.285c0 4.858-3.952 8.811-8.81 8.811h-360.38c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h360.38c13.129 0 23.81-10.682 23.81-23.811v-403.12c0-13.128-10.681-23.81-23.81-23.81z"/><path d="m185.41 134.28h-131.91c-7.444 0-13.5 6.056-13.5 13.5v66.13c0 7.444 6.056 13.5 13.5 13.5h405c7.444 0 13.5-6.056 13.5-13.5v-66.13c0-7.444-6.056-13.5-13.5-13.5h-238.09c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5h236.59v63.13h-321.142v-63.13h49.552c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5zm-64.552 78.13h-65.858v-63.13h65.858z"/><path d="m67.935 188.478 11.756 9.094c1.321 1.021 2.937 1.567 4.589 1.567.329 0 .66-.021.989-.065 1.986-.265 3.783-1.312 4.992-2.909l18.666-24.671c2.499-3.304 1.847-8.007-1.456-10.507-3.304-2.499-8.006-1.847-10.507 1.456l-14.092 18.626-5.76-4.456c-3.275-2.533-7.985-1.934-10.521 1.344-2.534 3.275-1.933 7.985 1.344 10.521z"/><path d="m153.684 180.845c0 4.143 3.357 7.5 7.5 7.5h72.66c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-72.66c-4.143 0-7.5 3.357-7.5 7.5z"/><path d="m431.671 188.345c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-162.823c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5z"/><path d="m53.504 441.366h404.992c7.444 0 13.5-6.056 13.5-13.5v-66.132c0-7.444-6.056-13.5-13.5-13.5h-404.992c-7.444 0-13.5 6.056-13.5 13.5v66.132c0 7.445 6.056 13.5 13.5 13.5zm403.492-15h-321.138v-63.132h321.138zm-401.992-63.132h65.854v63.132h-65.854z"/><path d="m98.342 394.8 6.604-6.604c2.929-2.93 2.929-7.678 0-10.607-2.93-2.928-7.678-2.928-10.607 0l-6.604 6.604-6.604-6.604c-2.93-2.928-7.678-2.928-10.607 0-2.929 2.93-2.929 7.678 0 10.607l6.604 6.604-6.604 6.604c-2.929 2.93-2.929 7.678 0 10.607 1.465 1.464 3.385 2.196 5.304 2.196s3.839-.732 5.304-2.196l6.604-6.604 6.604 6.604c1.465 1.464 3.385 2.196 5.304 2.196s3.839-.732 5.304-2.196c2.929-2.93 2.929-7.678 0-10.607z"/><path d="m161.184 402.3h72.66c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-72.66c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5z"/><path d="m268.848 402.3h162.823c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-162.823c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5z"/><path d="m458.496 241.257h-404.992c-7.444 0-13.5 6.056-13.5 13.5v66.132c0 7.444 6.056 13.5 13.5 13.5h404.992c7.444 0 13.5-6.056 13.5-13.5v-66.132c0-7.445-6.056-13.5-13.5-13.5zm-403.492 15h65.854v63.132h-65.854zm401.992 63.132h-321.138v-63.132h321.138z"/><path d="m107.471 269.506c-3.304-2.499-8.006-1.847-10.507 1.456l-14.092 18.626-5.76-4.456c-3.275-2.534-7.985-1.933-10.521 1.344-2.534 3.276-1.933 7.986 1.344 10.521l11.756 9.094c1.321 1.021 2.937 1.567 4.589 1.567.329 0 .66-.021.989-.065 1.986-.265 3.783-1.312 4.992-2.909l18.666-24.671c2.499-3.304 1.846-8.007-1.456-10.507z"/><path d="m359.011 295.322h72.66c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-72.66c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5z"/><path d="m161.184 295.322h162.823c4.143 0 7.5-3.357 7.5-7.5s-3.357-7.5-7.5-7.5h-162.823c-4.143 0-7.5 3.357-7.5 7.5s3.357 7.5 7.5 7.5z"/><g><circle cx="42.069" cy="69.954" r="7.5"/><circle cx="76.192" cy="69.954" r="7.5"/><circle cx="110.31" cy="69.954" r="7.5"/></g></g><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/><g/></svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,12 +0,0 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6311_9544)">
<path d="M31.5182 96.3591C31.7232 97.3748 32.7129 98.0311 33.7269 97.8261C38.9609 96.7696 42.8322 94.7671 45.6829 92.5183C48.5337 94.7671 52.4049 96.7698 57.6389 97.8261C58.6529 98.0311 59.6429 97.3746 59.8477 96.3591C60.0524 95.3441 59.3957 94.3551 58.3807 94.1503C54.1179 93.2898 50.8949 91.7473 48.4574 89.9433C51.1487 87.0021 52.9734 83.4338 53.8114 79.8881H58.3314C59.3672 79.8881 60.2064 79.0486 60.2064 78.0131C60.2064 76.9776 59.3672 76.1381 58.3314 76.1381H47.5584V71.7363C47.5584 70.7008 46.7192 69.8613 45.6834 69.8613C44.6477 69.8613 43.8084 70.7008 43.8084 71.7363V76.1381H33.0344C31.9987 76.1381 31.1594 76.9776 31.1594 78.0131C31.1594 79.0486 31.9987 79.8881 33.0344 79.8881H37.5544C38.3907 83.4258 40.2092 86.9866 42.8907 89.9241C40.4537 91.7403 37.2347 93.2923 32.9852 94.1501C31.9699 94.3548 31.3132 95.3441 31.5182 96.3591ZM41.4162 79.8881H49.9329C49.3807 81.7618 48.1822 84.6693 45.6567 87.4271C43.1337 84.6718 41.9552 81.7671 41.4162 79.8881Z" fill="black"/>
<path d="M82.3162 18.748C81.5372 18.748 80.839 19.23 80.5627 19.9585L71.3715 44.2103C71.0045 45.1785 71.492 46.261 72.4602 46.628C73.429 46.995 74.5112 46.5073 74.8782 45.5393L76.6965 40.7418C76.7585 40.748 76.8212 40.7513 76.885 40.7513H87.84C87.873 40.7513 87.905 40.748 87.9375 40.7463L89.754 45.539C90.1212 46.5075 91.2042 46.9945 92.172 46.6278C93.1402 46.2608 93.6277 45.1783 93.2607 44.21L84.0695 19.9583C83.7935 19.23 83.0955 18.748 82.3162 18.748ZM78.1142 37.0013L82.3162 25.9138L86.5182 37.0013H78.1142Z" fill="black"/>
<path d="M11.7858 116.457H16.6226C18.1479 116.457 19.5237 117.42 20.0462 118.853L22.5182 125.628C23.0439 127.069 24.3747 128 25.9087 128C27.4427 128 28.7737 127.069 29.2997 125.628L31.7715 118.852C32.2943 117.42 33.67 116.457 35.195 116.457H53.6349C54.6707 116.457 55.5099 115.618 55.5099 114.582C55.5099 113.547 54.6707 112.707 53.6349 112.707H35.1953C32.1008 112.707 29.3092 114.66 28.2487 117.567L25.9092 123.98L23.5694 117.567C22.5089 114.66 19.7174 112.707 16.6229 112.707H11.7858C7.35481 112.707 3.75003 109.103 3.75003 104.672V62.0325C3.75003 57.6015 7.35481 53.9967 11.7858 53.9967H36.633V54.4249C36.633 60.9237 41.9201 66.2108 48.4189 66.2108H87.6172V104.672C87.6172 109.103 84.0122 112.708 79.5811 112.708H62.3852C61.3495 112.708 60.5102 113.547 60.5102 114.583C60.5102 115.618 61.3495 116.458 62.3852 116.458H79.5811C86.0799 116.458 91.3672 111.171 91.3672 104.672V66.2108H92.805C94.33 66.2108 95.706 67.1735 96.2285 68.606L98.7003 75.3813C99.2263 76.8226 100.558 77.7536 102.091 77.7536C103.626 77.7536 104.956 76.8221 105.482 75.3813L107.954 68.606C108.477 67.1735 109.852 66.2108 111.377 66.2108H116.214C122.713 66.2108 128 60.9237 128 54.4249V38.8768C128 37.8413 127.161 37.0018 126.125 37.0018C125.089 37.0018 124.25 37.8413 124.25 38.8768V54.4249C124.25 58.856 120.645 62.4607 116.214 62.4607H111.377C108.283 62.4607 105.491 64.4138 104.431 67.3205L102.091 73.7338L99.7515 67.3205C98.6908 64.4138 95.899 62.4607 92.805 62.4607C91.289 62.4607 50.0484 62.4607 48.4189 62.4607C43.9878 62.4607 40.3831 58.856 40.3831 54.4249C40.3831 52.6232 40.3831 14.9124 40.3831 11.7856C40.3831 7.35481 43.9878 3.75003 48.4186 3.75003H116.214C120.645 3.75003 124.25 7.35481 124.25 11.7858V30.1267C124.25 31.1622 125.089 32.0018 126.125 32.0018C127.161 32.0018 128 31.1622 128 30.1267V11.7858C128 5.28704 122.713 0 116.214 0H48.4186C41.9201 0 36.6328 5.28704 36.6328 11.7858V50.2466H11.7858C5.28729 50.2466 0 55.5337 0 62.0325V104.672C0 111.17 5.28729 116.457 11.7858 116.457Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_6311_9544">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m291.532 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m254.455 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m217.378 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m123.925 51.273-76.763-.057c-.002 0-.003 0-.005 0-4.14 0-7.497 3.354-7.5 7.494-.003 4.143 3.353 7.503 7.495 7.506l76.763.057h.005c4.14 0 7.497-3.354 7.5-7.494.002-4.142-3.353-7.503-7.495-7.506z"/><path d="m287.507 124.863h-111.062c-6.933 0-12.574 5.641-12.574 12.574v33.479c0 6.948 5.641 12.602 12.574 12.602h111.062c6.933 0 12.574-5.653 12.574-12.602v-33.479c0-6.933-5.641-12.574-12.574-12.574zm-2.426 43.655h-106.209v-28.654h106.209z"/><path d="m292.581 210.217h-121.209c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h121.209c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m189.882 245.736h-18.51c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h18.51c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m292.581 245.736h-62.05c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h62.05c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m444.802 450.744h-.255c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h.255c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m490.667 301.75h-59.114v-129.049c0-11.669-9.494-21.163-21.163-21.163h-71.7v-114.592c0-11.404-9.278-20.682-20.682-20.682h-297.327c-11.404.001-20.681 9.278-20.681 20.682v241.718c0 11.419 9.277 20.709 20.681 20.709h33.801v97.131c0 11.669 9.494 21.163 21.163 21.163h104.223l-8.866 29.646h-14.994c-13.342 0-24.196 10.867-24.196 24.226v11.707c0 6.887 5.603 12.489 12.489 12.489h197.434c6.887 0 12.489-5.603 12.489-12.489v-11.707c0-13.358-10.867-24.226-24.225-24.226h-14.991l-8.847-29.646h71.443v56.735c0 11.763 9.557 21.333 21.305 21.333h91.758c11.763 0 21.333-9.57 21.333-21.333v-151.346c0-11.748-9.57-21.306-21.333-21.306zm-475.667-23.086v-177.438h137.039c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5h-137.039v-49.28c0-3.133 2.548-5.682 5.681-5.682h297.327c3.133 0 5.682 2.549 5.682 5.682v49.279h-137.011c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h137.011v177.438c0 3.147-2.549 5.709-5.682 5.709h-297.327c-3.133.001-5.681-2.56-5.681-5.708zm303.008 20.709c11.404 0 20.682-9.29 20.682-20.709v-112.126h71.7c3.398 0 6.163 2.765 6.163 6.163v129.049h-17.644c-11.748 0-21.305 9.558-21.305 21.306v41.683h-308.122v-65.365h248.526zm21.215 172.166v9.196h-192.411v-9.196c0-5.087 4.125-9.226 9.196-9.226h173.991c5.086 0 9.224 4.139 9.224 9.226zm-79.014-68.872c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h30.298l8.847 29.646h-112.696l8.866-29.646h30.188c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5h-150.067c-3.398 0-6.163-2.765-6.163-6.163v-16.766h308.123v22.929zm236.791 71.735c0 3.492-2.841 6.333-6.333 6.333h-91.758c-3.477 0-6.305-2.841-6.305-6.333v-151.346c0-3.477 2.828-6.306 6.305-6.306h91.758c3.492 0 6.333 2.829 6.333 6.306z"/><path d="m127.661 124.863h-76.451c-6.949 0-12.603 5.641-12.603 12.574v110.696c0 6.949 5.653 12.603 12.603 12.603h76.451c6.949 0 12.602-5.653 12.602-12.603v-110.695c0-6.934-5.653-12.575-12.602-12.575zm-2.398 120.873h-71.655v-105.873h71.655z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,378 +0,0 @@
(function (window) {
if (document.getElementById('activityChartContainer')) {
let currentType = 'service';
const tableContainer = document.getElementById('activityContainer');
const currentUserName = tableContainer.getAttribute('data-currentUserName');
const currentServiceId = tableContainer.getAttribute('data-currentServiceId');
const COLORS = {
delivered: '#0076d6',
failed: '#fa9441',
pending: '#C7CACE',
text: '#666'
};
const FONT_SIZE = 16;
const FONT_WEIGHT = 'bold';
const MAX_Y = 120;
const createChart = function(containerId, labels, deliveredData, failedData, pendingData) {
const container = d3.select(containerId);
container.selectAll('*').remove(); // Clear any existing content
const margin = { top: 60, right: 20, bottom: 40, left: 20 }; // Adjusted top margin for legend
const width = container.node().getBoundingClientRect().width - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = container.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
let tooltip = d3.select('#tooltip');
if (tooltip.empty()) {
tooltip = d3.select('body').append('div')
.attr('id', 'tooltip')
.style('display', 'none');
}
// Calculate total messages
const totalMessages = d3.sum(deliveredData) + d3.sum(failedData) + d3.sum(pendingData);
// Create legend only if there are messages
const legendContainer = d3.select('.chart-legend');
legendContainer.selectAll('*').remove(); // Clear any existing legend
if (totalMessages > 0) {
// Show legend if there are messages
const legendData = [
{ label: 'Delivered', color: COLORS.delivered },
{ label: 'Failed', color: COLORS.failed },
{ label: 'Pending', color: COLORS.pending }
];
const legendItem = legendContainer.selectAll('.legend-item')
.data(legendData)
.enter()
.append('div')
.attr('class', 'legend-item');
legendItem.append('div')
.attr('class', 'legend-rect')
.style('background-color', d => d.color)
.style('display', 'inline-block')
.style('margin-right', '5px');
legendItem.append('span')
.attr('class', 'legend-label')
.text(d => d.label);
// Ensure the legend is shown
legendContainer.style('display', 'flex');
} else {
// Hide the legend if there are no messages
legendContainer.style('display', 'none');
}
const x = d3.scaleBand()
.domain(labels)
.range([0, width])
.padding(0.1);
// Adjust the y-axis domain to add some space above the tallest bar
const maxY = d3.max(deliveredData.map((d, i) => d + (failedData[i] || 0) + (pendingData[i] || 0)));
const y = d3.scaleSymlog()
.domain([0, maxY + 2]) // Add 2 units of space at the top
.nice()
.range([height, 0]);
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Generate the y-axis with whole numbers
const yAxis = d3.axisLeft(y)
.ticks(Math.min(maxY + 2, 3))
.tickFormat(d3.format('d')); // Ensure whole numbers on the y-axis
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
// Data for stacking
const stackData = labels.map((label, i) => ({
label: label,
delivered: deliveredData[i],
failed: failedData[i] || 0,
pending: pendingData[i] || 0
}));
// Stack the data
const stack = d3.stack()
.keys(['delivered', 'failed', 'pending'])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
const series = stack(stackData);
// Color scale
const color = d3.scaleOrdinal()
.domain(['delivered', 'failed', 'pending'])
.range([COLORS.delivered, COLORS.failed, COLORS.pending]);
// Create bars with animation
const barGroups = svg.selectAll('.bar-group')
.data(series)
.enter()
.append('g')
.attr('class', 'bar-group')
.attr('fill', d => color(d.key));
const minBarHeight = 5;
barGroups.selectAll('rect')
.data(d => d)
.enter()
.append('rect')
.filter(d => d[1] - d[0] > 0)
.attr('x', d => x(d.data.label))
.attr('y', height)
.attr('height', 0)
.attr('width', x.bandwidth())
.on('mouseover', function(event, d) {
const key = d3.select(this.parentNode).datum().key;
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
tooltip.style('display', 'block')
.html(`${d.data.label}<br>${capitalizedKey}: ${d.data[key]}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
})
.transition()
.duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => {
const calculatedHeight = y(d[0]) - y(d[1]);
return calculatedHeight < minBarHeight ? minBarHeight : calculatedHeight;
}); };
// Function to create an accessible table
const createTable = function(tableId, chartType, labels, deliveredData, failedData, pendingData) {
const table = document.getElementById(tableId);
table.innerHTML = ""; // Clear previous data
const captionText = document.querySelector(`#${chartType} .chart-subtitle`).textContent;
const caption = document.createElement('caption');
caption.textContent = captionText;
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
// Create table header
const headerRow = document.createElement('tr');
const headers = ['Day', 'Delivered', 'Failed', 'Pending'];
headers.forEach(headerText => {
const th = document.createElement('th');
th.textContent = headerText;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
// Create table body
labels.forEach((label, index) => {
const row = document.createElement('tr');
const cellDay = document.createElement('td');
cellDay.textContent = label;
row.appendChild(cellDay);
const cellDelivered = document.createElement('td');
cellDelivered.textContent = deliveredData[index];
row.appendChild(cellDelivered);
const cellFailed = document.createElement('td');
cellFailed.textContent = failedData[index];
row.appendChild(cellFailed);
const cellPending = document.createElement('td');
cellPending.textContent = pendingData[index];
row.appendChild(cellPending);
tbody.appendChild(row);
});
table.appendChild(caption);
table.appendChild(thead);
table.append(tbody);
};
const fetchData = function(type) {
var ctx = document.getElementById('weeklyChart');
if (!ctx) {
return;
}
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
var url = type === 'service'
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
: `/services/${currentServiceId}/daily-stats-by-user.json?timezone=${encodeURIComponent(userTimezone)}`;
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
labels = [];
deliveredData = [];
failedData = [];
pendingData = [];
let totalMessages = 0;
for (var dateString in data) {
if (data.hasOwnProperty(dateString)) {
const dateParts = dateString.split('-');
const formattedDate = `${dateParts[1]}/${dateParts[2]}/${dateParts[0].slice(2)}`;
labels.push(formattedDate);
deliveredData.push(data[dateString].sms.delivered);
failedData.push(data[dateString].sms.failure);
pendingData.push(data[dateString].sms.pending || 0);
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure + data[dateString].sms.pending;
}
}
// Check if there are no messages sent
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
if (totalMessages === 0) {
// Remove existing chart and render the alert message
d3.select('#weeklyChart').selectAll('*').remove();
d3.select('#weeklyChart')
.append('div')
.html(`
<div class="usa-alert usa-alert--info usa-alert--slim">
<div class="usa-alert__body">
<p class="usa-alert__text">
No messages sent in the last 7 days
</p>
</div>
</div>
`);
// Hide the subtitle
if (subTitle) {
subTitle.style.display = 'none';
}
} else {
// If there are messages, create the chart and table
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
}
return data;
})
.catch(error => console.error('Error fetching daily stats:', error));
};
setInterval(() => fetchData(currentType), 25000);
const handleDropdownChange = function(event) {
const selectedValue = event.target.value;
currentType = selectedValue;
const subTitle = document.querySelector(`#activityChartContainer .chart-subtitle`);
const selectElement = document.getElementById('options');
const selectedText = selectElement.options[selectElement.selectedIndex].text;
subTitle.textContent = `${selectedText} - last 7 days`;
fetchData(selectedValue);
const liveRegion = document.getElementById('aria-live-account');
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
const tableHeading = document.querySelector('#tableActivity h2');
const senderElements = [
document.querySelector('[data-column="sender"]'),
...document.querySelectorAll('[data-sender]')
];
const allRows = document.querySelectorAll('#activity-table tbody tr');
const table = document.getElementById('activity-table');
const caption = table.querySelector('caption');
if (selectedValue === 'individual') {
tableHeading.textContent = 'My activity';
caption.textContent = `Table showing the sent jobs for ${currentUserName}`;
senderElements.forEach(el => {
if (el) el.style.display = 'none';
});
allRows.forEach(row => row.style.display = 'none');
const userRows = Array.from(allRows).filter(row => {
const senderCell = row.querySelector('[data-sender]');
const rowSender = senderCell ? senderCell.dataset.sender : '';
return rowSender === currentUserName;
});
if (userRows.length > 0) {
userRows.slice(0, 5).forEach(row => {
row.style.display = '';
});
} else {
const emptyMessageRow = Array.from(allRows).find(row => {
return row.querySelector('.table-empty-message');
});
if (emptyMessageRow) {
emptyMessageRow.style.display = '';
}
}
} else {
tableHeading.textContent = 'Service activity';
caption.textContent = `Table showing the sent jobs for service`;
senderElements.forEach(el => {
if (el) el.style.display = '';
});
allRows.forEach((row, index) => {
row.style.display = (index < 5) ? '' : 'none';
});
}
};
document.addEventListener('DOMContentLoaded', function() {
// Initialize activityChart chart and table with service data by default
fetchData(currentType);
const allRows = Array.from(document.querySelectorAll('#activity-table tbody tr'));
allRows.forEach((row, index) => {
row.style.display = (index < 5) ? '' : 'none';
});
const dropdown = document.getElementById('options');
dropdown.addEventListener('change', handleDropdownChange);
});
// Resize chart on window resize
window.addEventListener('resize', function() {
if (labels.length > 0 && deliveredData.length > 0 && failedData.length > 0 && pendingData.length > 0) {
createChart('#weeklyChart', labels, deliveredData, failedData, pendingData);
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData, pendingData);
}
});
// Export functions for testing
window.createChart = createChart;
window.createTable = createTable;
window.handleDropdownChange = handleDropdownChange;
window.fetchData = fetchData;
}
})(window);

View File

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

View File

@@ -63,9 +63,9 @@
const buttonContent = this.buttonContent[buttonState](this.fieldLabel);
const stickyClass = expanded ? ' js-stick-at-bottom-when-scrolling' : '';
return $(`<div class="selection-footer${stickyClass} margin-top-2">
return $(`<div class="selection-footer${stickyClass}">
<button
class="usa-button usa-button--outline selection-footer__button"
class="govuk-button govuk-button--secondary selection-footer__button usa-button usa-button--outline"
aria-expanded="${expanded ? 'true' : 'false'}"
aria-controls="${this.fieldsetId}">
${buttonContent}
@@ -78,6 +78,8 @@
this.module.$formGroup.append(this.$el);
// make footer sticky if expanded, clear up from it being sticky if not
GOVUK.stickAtBottomWhenScrolling.recalculate();
};
function CollapsibleCheckboxes () {}
@@ -95,7 +97,6 @@
this.total = this.$checkboxes.length;
this.legendText = this.$fieldset.find('legend').first().text().trim();
this.expanded = false;
this.checkUncheckButtonsAdded = false;
this.addHeadingHideLegend();
@@ -123,45 +124,6 @@
this.$fieldset.find('legend').addClass('usa-sr-only');
};
CollapsibleCheckboxes.prototype.addCheckUncheckAllButtons = function() {
const $buttonsContainer = $('<div class="check-uncheck-all-buttons margin-bottom-2"></div>');
this.$toggleAllButton = $('<button type="button" class="usa-button usa-button--outline usa-button--small">Select all</button>');
$buttonsContainer.append(this.$toggleAllButton);
this.summary.$el.after($buttonsContainer);
this.$toggleAllButton.on('click', this.toggleAll.bind(this));
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.toggleAll = function(e) {
e.preventDefault();
e.stopPropagation();
const allChecked = this.$checkboxes.filter(':checked').length === this.$checkboxes.length;
if (allChecked) {
this.$checkboxes.prop('checked', false);
} else {
this.$checkboxes.prop('checked', true);
}
this.handleSelection();
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.updateToggleButtonText = function() {
if (!this.$toggleAllButton) return;
const checkedCount = this.$checkboxes.filter(':checked').length;
const allChecked = checkedCount === this.$checkboxes.length;
if (allChecked) {
this.$toggleAllButton.text('Deselect all');
} else {
this.$toggleAllButton.text('Select all');
}
};
CollapsibleCheckboxes.prototype.expand = function(e) {
if (e !== undefined) { e.preventDefault(); }
@@ -170,15 +132,6 @@
this.expanded = true;
this.summary.update(this.getSelection());
this.footer.update(this.expanded);
if (!this.checkUncheckButtonsAdded) {
this.addCheckUncheckAllButtons();
this.checkUncheckButtonsAdded = true;
} else {
if (this.$toggleAllButton) {
this.$toggleAllButton.parent().show();
}
}
}
// shift focus whether expanded or not
@@ -192,10 +145,6 @@
this.expanded = false;
this.summary.update(this.getSelection());
this.footer.update(this.expanded);
if (this.$toggleAllButton) {
this.$toggleAllButton.parent().hide();
}
}
// shift focus whether expanded or not
@@ -210,12 +159,11 @@
};
CollapsibleCheckboxes.prototype.handleSelection = function(e) {
this.summary.update(this.getSelection(), this.total, this.fieldLabel);
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.bindEvents = function() {
const self = this;
this.$formGroup.on('click', '.usa-button', this.handleClick.bind(this));
this.$formGroup.on('click', '.govuk-button', this.handleClick.bind(this));
this.$checkboxes.on('click', this.handleSelection.bind(this));
this.summary.bindEvents(this);

View File

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

View File

@@ -80,14 +80,14 @@
$('.copy-to-clipboard__value', component)[0], () =>
$component
.html(states.valueCopied(stateOptions))
.find('.usa-button').focus()
.find('.govuk-button').focus()
)
)
.on(
'click', '.copy-to-clipboard__button--show', () =>
$component
.html(states.valueVisible(stateOptions))
.find('.usa-button').focus()
.find('.govuk-button').focus()
);
if ('stickAtBottomWhenScrolling' in GOVUK) {

View File

@@ -1,69 +1,34 @@
function announceUploadStatusFromElement() {
const srRegion = document.getElementById('upload-status-live');
const success = document.getElementById('upload-success');
const error = document.getElementById('upload-error');
if (!srRegion) return;
const message = error?.textContent || success?.textContent;
if (message) {
srRegion.textContent = '';
setTimeout(() => {
srRegion.textContent = message + '\u00A0'; // add a non-breaking space
srRegion.focus(); // Optional
}, 300);
}
}
// Exported for use in tests
function initUploadStatusAnnouncer() {
document.addEventListener('DOMContentLoaded', () => {
announceUploadStatusFromElement();
});
}
(function(Modules) {
"use strict";
Modules.FileUpload = function() {
this.submit = () => this.$form.trigger('submit');
this.showCancelButton = () => {
$('.file-upload-button', this.$form).replaceWith(`
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
Uploading<span class="dot-anim" aria-hidden="true"></span>
</button>
`);
};
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
`);
this.start = function(component) {
this.$form = $(component);
this.$form.on('click', '[data-module="upload-trigger"]', function () {
const inputId = $(this).data('file-input-id');
const fileInput = document.getElementById(inputId);
if (fileInput) fileInput.click();
});
// The label gets styled like a button and is used to hide the native file upload control. This is so that
// users see a button that looks like the others on the site.
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
// Clear the form if the user navigates back to the page
$(window).on("pageshow", () => this.$form[0].reset());
this.$form.on('change', '.file-upload-field', () => {
this.submit();
this.showCancelButton();
});
// Need to put the event on the container, not the input for it to work properly
this.$form.on(
'change', '.file-upload-field',
() => this.submit() && this.showCancelButton()
);
};
};
})(window.GOVUK.Modules);
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
announceUploadStatusFromElement,
initUploadStatusAnnouncer
};
}
if (typeof window !== 'undefined') {
initUploadStatusAnnouncer();
}

View File

@@ -22,6 +22,7 @@
this.$scrollableTable
.on('scroll', this.toggleShadows)
.on('scroll', this.maintainHeight)
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
if (
window.GOVUK.stickAtBottomWhenScrolling &&
@@ -36,11 +37,11 @@
this.insertShims = () => {
const attributesForFocus = 'role aria-labelledby';
const attributesForFocus = 'role aria-labelledby tabindex';
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
this.$table.find('caption').attr('id', captionId);
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`);
this.$component
.append(

View File

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

View File

@@ -17,7 +17,6 @@
this.minEntries = 2;
this.listItemName = this.$wrapper.data('listItemName');
this.getSharedAttributes();
this.getOriginalClasses();
this.getValues();
this.maxEntries = this.entries.length;
@@ -26,33 +25,31 @@
this.bindEvents();
};
ListEntry.optionalAttributes = ['aria-describedby'];
ListEntry.prototype.renderEntry = function(data) {
return `
<div class="list-entry">
<label for="${data.id}" class="usa-label">
<span class="usa-sr-only">${data.listItemName} number </span>${data.number}.
</label>
<input
class="usa-input ${data.classes || ''}"
name="${data.name}"
id="${data.id}"
${data.value ? `value="${data.value}"` : ''}
${data.sharedAttributes}
/>
${data.button ? `
<button type="button" class="usa-button usa-button--unstyled input-list__button--remove">
Remove<span class="usa-sr-only"> ${data.listItemName} number ${data.number}</span>
</button>
` : ''}
</div>
`;
};
ListEntry.prototype.renderAddButton = function(data) {
return `<button type="button" class="usa-button usa-button--outline input-list__button--add margin-top-4">Add another ${data.listItemName} (${data.entriesLeft} remaining)</button>`;
};
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}}.' +
'</label>' +
'<input' +
' name="{{name}}"' +
' id="{{id}}"' +
' {{#value}}value="{{value}}{{/value}}"' +
' {{{sharedAttributes}}}' +
'/>' +
'{{#button}}' +
'<button type="button" class="govuk-button govuk-button--secondary input-list__button--remove">' +
'Remove<span class="usa-sr-only"> {{listItemName}} number {{number}}</span>' +
'</button>' +
'{{/button}}' +
'</div>'
);
ListEntry.prototype.addButtonTemplate = Hogan.compile(
'<button type="button" class="govuk-button govuk-button--secondary input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
);
ListEntry.prototype.getSharedAttributes = function () {
var $inputs = this.$wrapper.find('input'),
generatedAttributes = ['id', 'name', 'value', 'class'],
attributeTemplate = Hogan.compile(' {{name}}="{{value}}"'),
generatedAttributes = ['id', 'name', 'value'],
attributes = [],
attrIdx,
elmAttributes,
@@ -71,7 +68,7 @@
while (attrIdx--) {
// prevent duplicates
if ($.inArray(elmAttrs[attrIdx].name, existingAttributes) === -1) {
attrStr += ` ${elmAttrs[attrIdx].name}="${elmAttrs[attrIdx].value}"`;
attrStr += attributeTemplate.render({ 'name': elmAttrs[attrIdx].name, 'value': elmAttrs[attrIdx].value });
existingAttributes.push(elmAttrs[attrIdx].name);
}
}
@@ -97,20 +94,6 @@
this.sharedAttributes = (attributes.length) ? getAttributesHTML(attributes) : '';
};
ListEntry.prototype.getOriginalClasses = function () {
var $firstInput = this.$wrapper.find('input').first();
if ($firstInput.length) {
var classList = $firstInput.attr('class');
if (classList) {
// Preserve any additional classes from the original input
this.additionalClasses = classList;
} else {
this.additionalClasses = '';
}
} else {
this.additionalClasses = '';
}
};
ListEntry.prototype.getValues = function () {
this.entries = [];
this.$wrapper.find('input').each(function (idx, elm) {
@@ -199,17 +182,16 @@
'name' : this.getId(entryNumber),
'value' : entry,
'listItemName' : this.listItemName,
'sharedAttributes': this.sharedAttributes,
'classes': this.additionalClasses
'sharedAttributes': this.sharedAttributes
};
if (entryNumber > 1) {
dataObj.button = true;
}
this.$wrapper.append(this.renderEntry(dataObj));
this.$wrapper.append(this.entryTemplate.render(dataObj));
}.bind(this));
if (this.entries.length < this.maxEntries) {
this.$wrapper.append(this.renderAddButton({
this.$wrapper.append(this.addButtonTemplate.render({
'listItemName' : this.listItemName,
'entriesLeft' : (this.maxEntries - this.entries.length)
}));

View File

@@ -15,11 +15,11 @@
let query = normalize($searchBox.val());
let results = 0;
let $noResultsMessage = $('.js-live-search-no-results');
$targets.each(function() {
let content = $('.live-search-relevant', this).text() || $(this).text();
let isMatch = normalize(content).indexOf(normalize(query)) > -1;
if ($(this).has(':checked').length) {
$(this).show();
@@ -28,27 +28,17 @@
}
if (query == '') {
$(this).removeClass('js-hidden');
$(this).css('display', '');
results++;
return;
}
let isMatch = normalize(content).includes(normalize(query));
$(this).toggle(isMatch);
if (isMatch) { results++; }
if (isMatch) {
$(this).removeClass('js-hidden');
results++;
} else {
$(this).addClass('js-hidden');
}
});
if (query !== '' && results === 0) {
$noResultsMessage.removeClass('js-hidden');
} else {
$noResultsMessage.addClass('js-hidden');
}
if (state === 'loaded') {
if (query !== '') {
$searchBox.attr('aria-label', $searchLabel.text().trim() + ', ' + resultsSummary(results));

View File

@@ -1,5 +1,7 @@
window.GOVUK.Frontend.initAll();
$(() => $("time.timeago").timeago());
var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init();
@@ -7,6 +9,9 @@ $(() => GOVUK.modules.start());
$(() => $('.error-message, .usa-error-message').eq(0).parent('label').next('input').trigger('focus'));
$(() => $('.govuk-header__container').on('click', function() {
$(this).css('border-color', '#005ea5');
}));
// Applies our expanded focus style to the siblings of links when that link is wrapped in a heading.
//

View File

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

View File

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

View File

@@ -3,70 +3,65 @@
"use strict";
var Modules = global.GOVUK.Modules;
var Hogan = global.Hogan;
// Template functions for rendering component states
let renderStates = {
'initial': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
// Object holding all the states for the component's HTML
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>
` : ''}
<div class="radio-select__column margin-y-2">
${data.categories.map(category =>
`<input type='button' class='usa-button usa-button--outline radio-select__button--category' aria-expanded="false" value='${category}' />`
).join('')}
</div>
`;
},
'choose': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
{{#categories}}
<input type='button' class='usa-button usa-button--outline 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>
` : ''}
<div class="radio-select__column margin-y-2">
${data.choices.map(choice => `
<div class="usa-radio js-option">
<input class="usa-radio__input" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
<input type='button' class='usa-button usa-button--outline radio-select__button--done margin-top-4' aria-expanded='true' value='Back to select a new time' />
</div>
`;
},
'chosen': function(data) {
return `
${data.showNowAsDefault ? `
<div class="radio-select__column margin-y-2">
<div class="usa-radio">
<input class="usa-radio__input" id="${data.name}-0" name="${data.name}" type="radio" value="">
<label class="usa-radio__label" for="${data.name}-0">Now</label>
</div>
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
{{#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>
` : ''}
{{/choices}}
<input type='button' class='usa-button usa-button--outline radio-select__button--done' aria-expanded='true' value='Done' />
</div>
`),
'chosen': Hogan.compile(`
{{#showNowAsDefault}}
<div class="radio-select__column margin-y-2">
${data.choices.map(choice => `
<div class="usa-radio">
<input class="usa-radio__input" checked="checked" type="radio" value="${choice.value}" id="${choice.id}" name="${data.name}" />
<label class="usa-radio__label" for="${choice.id}">${choice.label}</label>
</div>
`).join('')}
<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>
</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>
`;
}
{{/showNowAsDefault}}
<div class="radio-select__column margin-y-2">
{{#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>
{{/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>
`)
};
let shiftFocus = function(elementToFocus, component) {
@@ -85,7 +80,7 @@
let $component = $(component);
let render = (state, data) => {
$component.html(renderStates[state](data));
$component.html(states[state].render(data));
};
// store array of all options in component
let choices = $('label', $component).toArray().map(function(element) {

View File

@@ -1,13 +0,0 @@
document.querySelectorAll('.usa-button-group a, .usa-pagination a').forEach(function(button) {
button.addEventListener('click', function() {
sessionStorage.setItem('scrollPosition', window.pageYOffset);
});
});
document.addEventListener('DOMContentLoaded', function() {
var scrollPosition = sessionStorage.getItem('scrollPosition');
if (scrollPosition !== null) {
window.scrollTo(0, parseInt(scrollPosition));
sessionStorage.removeItem('scrollPosition');
}
});

View File

@@ -1,57 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const sidenavItems = document.querySelectorAll('.usa-sidenav__item > .parent-link');
let lastPath = window.location.pathname;
let debounceTimeout = null;
sidenavItems.forEach((link) => {
const parentItem = link.parentElement;
const sublist = parentItem.querySelector('.usa-sidenav__sublist');
const targetHref = link.getAttribute('href');
// initialize the menu to open the correct submenu based on the current route
if (window.location.pathname.startsWith(targetHref)) {
parentItem.classList.add('open');
link.setAttribute('aria-expanded', 'true');
}
link.addEventListener('click', (event) => {
const currentPath = window.location.pathname;
// prevent default behavior only if navigating to the same route
if (currentPath === targetHref) {
event.preventDefault();
return;
}
if (sublist && !parentItem.classList.contains('open')) {
// debounce the menu update to avoid flickering
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
parentItem.classList.add('open');
link.setAttribute('aria-expanded', 'true');
}, 50);
}
});
});
// handle browser back/forward navigation
window.addEventListener('popstate', () => {
const currentPath = window.location.pathname;
// sync menu state
sidenavItems.forEach((link) => {
const parentItem = link.parentElement;
const targetHref = link.getAttribute('href');
if (currentPath.startsWith(targetHref)) {
parentItem.classList.add('open');
link.setAttribute('aria-expanded', 'true');
} else {
parentItem.classList.remove('open');
link.setAttribute('aria-expanded', 'false');
}
});
lastPath = currentPath;
});
});

View File

@@ -1,83 +0,0 @@
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
document.addEventListener('DOMContentLoaded', function () {
const isJobPage = window.location.pathname.includes('/jobs/');
if (!isJobPage) return;
const jobEl = document.querySelector('[data-job-id]');
const jobId = jobEl?.dataset?.jobId;
const featureEnabled = jobEl?.dataset?.feature === 'true';
const apiHost = jobEl?.dataset?.host;
if (!jobId) return;
if (featureEnabled) {
const socket = io(apiHost);
socket.on('connect_error', (err) => {
console.error('Socket connect_error:', err);
});
socket.on('error', (err) => {
console.error('Socket error:', err);
});
socket.on('connect', () => {
socket.emit('join', { room: `job-${jobId}` });
});
window.addEventListener('beforeunload', () => {
socket.emit('leave', { room: `job-${jobId}` });
});
const debouncedUpdate = debounce((data) => {
updateAllJobSections();
}, 1000);
socket.on('job_updated', (data) => {
if (data.job_id !== jobId) return;
debouncedUpdate(data);
});
}
function updateAllJobSections() {
const resourceEl = document.querySelector('[data-socket-update="status"]');
const url = resourceEl?.dataset?.resource;
if (!url) {
console.warn('No resource URL found for job updates');
return;
}
fetch(url)
.then((res) => res.json())
.then(({ status, counts, notifications }) => {
const sections = {
status: document.querySelector('[data-socket-update="status"]'),
counts: document.querySelector('[data-socket-update="counts"]'),
notifications: document.querySelector(
'[data-socket-update="notifications"]'
),
};
if (status && sections.status) {
sections.status.innerHTML = status;
}
if (counts && sections.counts) {
sections.counts.innerHTML = counts;
}
if (notifications && sections.notifications) {
sections.notifications.innerHTML = notifications;
}
})
.catch((err) => {
console.error('Error fetching job update partials:', err);
});
}
});

View File

@@ -197,11 +197,6 @@
if (event.currentTarget.value === 'add-new-template' && this.$singleNotificationChannel) {
window.location = "/services/" + this.$singleChannelService + "/templates/add-" + this.$singleNotificationChannel;
} else {
if (this.currentState === 'add-new-template') {
this.$form.find('input[type=checkbox]').prop('checked', false);
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
}
if (this.stateChanged()) {
this.render();
}
@@ -291,29 +286,7 @@
mode = 'dialog';
}
if (this.currentState === 'add-new-template') {
this.$form.find('.template-list-item').addClass('js-hidden');
$('.live-search').addClass('js-hidden');
$('#breadcrumb-template-folders').addClass('js-hidden');
$('#template-list').addClass('js-hidden');
this.$form.find('input[type=checkbox]').prop('checked', false);
this.selectionStatus.update({ total: 0, templates: 0, folders: 0 });
$('#page-title').text('New Template');
$('#page-description').text('Every message starts with a template. Choose to start with a blank template or copy an existing template.');
document.title = 'New Templates';
} else {
this.$form.find('.template-list-item').removeClass('js-hidden');
$('.live-search').removeClass('js-hidden');
$('#breadcrumb-template-folders').removeClass('js-hidden');
$('#template-list').removeClass('js-hidden');
$('#page-title').text('Select or create a template');
$('#page-description').text('Every message starts with a template. To send, choose or create a template.');
document.title = 'Select or create a template';
}
if (currentStateObj && 'setFocus' in currentStateObj) {
if (currentStateObj && ('setFocus' in currentStateObj)) {
scrollTop = $(window).scrollTop();
currentStateObj.setFocus();
$(window).scrollTop(scrollTop);

View File

@@ -1,150 +0,0 @@
(function (window) {
function createTotalMessagesChart() {
var chartContainer = document.getElementById('totalMessageChartContainer');
if (!chartContainer) return;
var chartTitle = document.getElementById('chartTitle').textContent;
// Access data attributes from the HTML
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
var totalMessages = messagesSent + messagesRemaining;
// Update the message below the chart
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
var minSentValue = totalMessages * minSentPercentage;
var displaySent = Math.max(messagesSent, minSentValue);
var displayRemaining = totalMessages - displaySent;
var svg = d3.select("#totalMessageChart");
var width = chartContainer.clientWidth;
var height = 48;
// Ensure the width is set correctly
if (width === 0) {
console.error('Chart container width is 0, cannot set SVG width.');
return;
}
svg.attr("width", width).attr("height", height);
var x = d3.scaleLinear()
.domain([0, totalMessages])
.range([0, width]);
// Create tooltip dynamically
var tooltip = d3.select("body").append("div")
.attr("id", "tooltip");
// Create the initial bars
var sentBar = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("fill", '#0076d6')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Messages Sent: ${messagesSent.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
var remainingBar = svg.append("rect")
.attr("x", 0) // Initially set to 0, will be updated during animation
.attr("y", 0)
.attr("height", height)
.attr("fill", '#C7CACE')
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Remaining: ${messagesRemaining.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY - 20}px`);
})
.on('mouseout', function() {
tooltip.style('display', 'none');
});
// Animate the bars together as a single cohesive line
svg.transition()
.duration(1000) // Total animation duration
.attr("width", width)
.tween("resize", function() {
var interpolator = d3.interpolate(0, width);
return function(t) {
var newWidth = interpolator(t);
var sentWidth = x(displaySent) / width * newWidth;
var remainingWidth = x(displayRemaining) / width * newWidth;
sentBar.attr("width", sentWidth);
remainingBar.attr("x", sentWidth).attr("width", remainingWidth);
};
});
// Create and populate the accessible table
var tableContainer = document.getElementById('totalMessageTable');
var table = document.createElement('table');
table.className = 'usa-sr-only usa-table';
var caption = document.createElement('caption');
caption.textContent = chartTitle;
table.appendChild(caption);
var thead = document.createElement('thead'); // Ensure thead is created
var theadRow = document.createElement('tr');
var thMessagesSent = document.createElement('th');
thMessagesSent.textContent = 'Messages Sent'; // First column header
var thRemaining = document.createElement('th');
thRemaining.textContent = 'Remaining'; // Second column header
theadRow.appendChild(thMessagesSent);
theadRow.appendChild(thRemaining);
thead.appendChild(theadRow); // Append theadRow to the thead
table.appendChild(thead);
var tbody = document.createElement('tbody');
var tbodyRow = document.createElement('tr');
var tdMessagesSent = document.createElement('td');
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
var tdRemaining = document.createElement('td');
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
tbodyRow.appendChild(tdMessagesSent);
tbodyRow.appendChild(tdRemaining);
tbody.appendChild(tbodyRow);
table.appendChild(tbody);
tableContainer.appendChild(table);
table.appendChild(tbody);
tableContainer.appendChild(table);
// Ensure the chart resizes correctly on window resize
window.addEventListener('resize', function () {
width = chartContainer.clientWidth;
x.range([0, width]);
svg.attr("width", width);
sentBar.attr("width", x(displaySent));
remainingBar.attr("x", x(displaySent)).attr("width", x(displayRemaining));
});
}
// Initialize total messages chart if the container exists
document.addEventListener('DOMContentLoaded', function() {
createTotalMessagesChart();
});
// Export function for testing
window.createTotalMessagesChart = createTotalMessagesChart;
})(window);

View File

@@ -1,117 +0,0 @@
function showError(input, errorElement, message) {
errorElement.textContent = ""; // Clear existing message
errorElement.style.display = "block";
// Small delay to ensure screen readers pick up the change
setTimeout(() => {
errorElement.textContent = message;
}, 10);
if (input.type !== "radio" && input.type !== "checkbox") {
input.classList.add("usa-input--error");
}
input.setAttribute("aria-describedby", errorElement.id);
}
function hideError(input, errorElement) {
errorElement.style.display = "none";
if (input.type !== "radio" && input.type !== "checkbox") {
input.classList.remove("usa-input--error");
}
input.removeAttribute("aria-describedby");
}
function getFieldLabel(input) {
const label = document.querySelector(`label[for="${input.id}"]`);
return label ? label.textContent.trim() : "This field";
}
// Attach validation logic to forms
function attachValidation() {
const forms = document.querySelectorAll('form[data-force-focus="True"]');
forms.forEach((form) => {
const inputs = form.querySelectorAll("input, textarea, select");
form.addEventListener("submit", function (event) {
let isValid = true;
let firstInvalidInput = null;
const validatedRadioNames = new Set();
inputs.forEach((input) => {
if (input.type === "hidden") return;
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
let errorElement = document.getElementById(errorId);
if (!errorElement) {
errorElement = document.createElement("span");
errorElement.id = errorId;
errorElement.classList.add("usa-error-message");
errorElement.setAttribute("aria-live", "polite");
errorElement.style.display = "none";
if (input.type === "radio") {
const group = form.querySelectorAll(`input[name="${input.name}"]`);
const lastRadio = group[group.length - 1];
lastRadio.parentElement.insertAdjacentElement("afterend", errorElement);
} else {
input.insertAdjacentElement("afterend", errorElement);
}
}
if (input.type === "radio") {
if (validatedRadioNames.has(input.name)) return;
validatedRadioNames.add(input.name);
const radioGroup = form.querySelectorAll(`input[name="${input.name}"]`);
const isChecked = Array.from(radioGroup).some(radio => radio.checked);
if (!isChecked) {
showError(input, errorElement, `Error: A selection must be made.`);
isValid = false;
if (!firstInvalidInput) {
firstInvalidInput = input;
}
}
} else if (input.value.trim() === "") {
showError(input, errorElement, `Error: ${getFieldLabel(input)} is required.`);
isValid = false;
if (!firstInvalidInput) {
firstInvalidInput = input;
}
}
});
if (!isValid) {
event.preventDefault();
if (firstInvalidInput) firstInvalidInput.focus();
}
});
inputs.forEach((input) => {
if (input.type === "hidden") return;
input.addEventListener("input", function () {
const errorId = input.type === "radio" ? `${input.name}-error` : `${input.id}-error`;
const errorElement = document.getElementById(errorId);
if (errorElement && input.value.trim() !== "") {
hideError(input, errorElement);
}
});
if (input.type === "radio") {
input.addEventListener("change", function () {
const errorElement = document.getElementById(`${input.name}-error`);
if (errorElement) {
hideError(input, errorElement);
}
});
}
});
});
}
// Automatically attach validation only in the browser
if (typeof window !== "undefined") {
document.addEventListener("DOMContentLoaded", attachValidation);
}
// ✅ Check if we're in a Node.js environment (for Jest) before using `module.exports`
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = { showError, hideError, getFieldLabel, attachValidation };
}

View File

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

View File

@@ -1,4 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
document.cookie = `timezone=${timeZone}; path=/`;
})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,148 +0,0 @@
@use "uswds-core" as *;
$delivered: color('blue-50v');
$pending: color('green-cool-40v');
$failed: color('gray-cool-20');
.chart-container {
display: flex;
&.usage {
height: units(4);
}
svg {
overflow: visible;
}
}
#totalMessageChartContainer {
max-width: 600px;
}
.bar {
border-radius: units(0.5);
&.delivered, &.usage {
background-color: $delivered;
margin-right: 1px;
}
&.pending{
background-color: $pending;
margin-right: 1px;
}
&.failed, &.remaining {
background-color: $failed;
}
}
.legend {
display: flex;
margin: units(1) 0;
.legend-item {
display: flex;
align-items: flex-start;
margin-right: units(2);
}
.legend-color {
width: units(3);
height: units(3);
margin-right: 0;
padding: 0;
border-radius: 2px;
background-color: $delivered;
&.pending {
background-color: $pending;
}
&.failed, &.remaining {
background-color: $failed;
}
}
.legend-value {
margin: 0 units(1);
}
}
.progress-bar {
width: 300px;
height: 20px;
background-color: #eee;
border-radius: 5px;
margin-bottom: 10px;
}
.progress-bar-inner {
height: 100%;
background-color: #007bff;
border-radius: inherit;
}
.chart {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: units(1) units(1) 0;
}
.chart-subtitle {
font-size: size("body", 6);
font-weight: bold;
text-align: left;
width: 100%;
padding: 0;
margin: 0;
}
.axis text {
font-size: size("body", 1);
}
.axis line,
.axis path {
shape-rendering: crispEdges;
stroke: #000;
fill: none;
}
.bar {
fill-opacity: 0.8;
}
.chart-container {
width: 100%;
position: relative;
}
.chart-legend {
display: flex;
align-items: center;
}
.legend-item {
display: flex;
align-items: center;
margin-right: units(2);
.legend-rect {
width: units(2);
height: units(2);
margin-right: units(1);
}
}
#tooltip {
position: absolute;
display: none;
background: color('ink');
color: #FFF;
border: 1px solid #ccc;
padding: units(1);
border-radius: units(1);
pointer-events: none;
z-index: 100;
font-size: size("body", 3);
line-height: 1.3;
}

View File

@@ -8,7 +8,7 @@
width: 100%;
max-width: 464px;
box-sizing: border-box;
padding: units(2);
padding: units(1);
background: color('gray-cool-10');
border: 1px solid color('gray-cool-10');
border-radius: 5px;
@@ -49,12 +49,12 @@
}
.sms-message-sender, .sms-message-file-name, .sms-message-scheduler, .sms-message-template, .sms-message-sender {
margin: units(0.5) 0 0;
margin:0.25rem 0 0;
}
.sms-message-recipient {
color: color('gray-cool-90');
margin: units(0.5) 0 units(2);
margin: units(1) 0 units(1);
}
.sms-message-status {
@@ -66,10 +66,6 @@ h2.sms-message-header {
margin-bottom: 0.5rem;
}
.usa-prose >*+ h2.message-header {
margin-top: 1em;
}
h2.recipient-list {
margin-bottom: 0.5rem;
}
@@ -345,42 +341,10 @@ h2.recipient-list {
}
}
.usa-search__input, [type=search] {
border-right: 1px solid;
height: 40px;
margin-top: units(1);
}
.usa-search .search-form__button {
height: 40px;
}
// Button ellipses loading
.dot-anim {
display: inline-block;
margin-left: 0; /* remove left margin if it exists */
padding-left: 0;
font-size: 1em;
animation: dots 1s steps(3, end) infinite;
}
/* Optional: reduce spacing by removing whitespace node */
button span.dot-anim {
margin-left: 0; /* forces no space even if white-space exists */
}
.dot-anim::after {
content: '.';
animation: dotPulse 1.5s steps(3, end) infinite;
}
@keyframes dotPulse {
0% { content: ''; }
33% { content: '.'; }
66% { content: '..'; }
100% { content: '...'; }
}
.modal-open {
overflow: hidden;
.js-focus-style {
outline: 3px solid color("blue-40v");
box-shadow: 0 0 0 7px color("blue-40v");
*:focus {
outline: none;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,9 @@ in the form $setting: value,
@use "uswds-core" with (
$theme-font-type-sans: "public-sans",
$theme-card-border-radius: 0,
$theme-card-gap: 4,
$theme-card-border-width: 2px,
$theme-card-border-color: "ink",
$theme-show-notifications: false,
$theme-banner-max-width: "desktop-lg",
$theme-grid-container-max-width: "desktop-lg",
$theme-footer-max-width: "desktop-lg",
$theme-header-max-width: "desktop-lg",
$theme-identifier-max-width: "desktop-lg"
$theme-header-max-width: "desktop-lg"
);

View File

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

View File

@@ -8,19 +8,16 @@ from notifications_utils import DAILY_MESSAGE_LIMIT
class Config(object):
SIMULATED_SMS_NUMBERS = ("+14254147755", "+14254147167")
NOTIFY_APP_NAME = "admin"
NOTIFY_ENVIRONMENT = getenv("NOTIFY_ENVIRONMENT", "development")
API_HOST_NAME = getenv("API_HOST_NAME", "localhost")
API_PUBLIC_URL = getenv("API_PUBLIC_URL", "localhost")
API_PUBLIC_WS_URL = getenv("API_PUBLIC_WS_URL", "localhost")
ADMIN_BASE_URL = getenv("ADMIN_BASE_URL", "http://localhost:6012")
HEADER_COLOUR = "#81878b" # mix of dark-grey and mid-grey
HEADER_COLOUR = (
"#81878b" # mix(govuk-colour("dark-grey"), govuk-colour("mid-grey"))
)
LOGO_CDN_DOMAIN = (
"static-logos.notifications.service.gov.uk" # TODO use our own CDN
)
ASSETS_DEBUG = False
# Credentials
@@ -41,7 +38,7 @@ class Config(object):
NR_MONITOR_ON = settings and settings.monitor_mode
COMMIT_HASH = getenv("COMMIT_HASH", "--------")[0:7]
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov", "mil", "si.edu"]
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov"]
# Logging
NOTIFY_LOG_LEVEL = getenv("NOTIFY_LOG_LEVEL", "INFO")
@@ -56,13 +53,7 @@ class Config(object):
PERMANENT_SESSION_LIFETIME = 1800 # 30 Minutes
SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year
REPLY_TO_EMAIL_ADDRESS_VALIDATION_TIMEOUT = 45
ACTIVITY_STATS_LIMIT_DAYS = {
"today": 0,
"one_day": 1,
"three_day": 3,
"five_day": 5,
"seven_day": 7,
}
ACTIVITY_STATS_LIMIT_DAYS = 7
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_NAME = "notify_admin_session"
SESSION_COOKIE_SECURE = True
@@ -90,8 +81,6 @@ class Config(object):
],
}
FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "false") == "true"
def _s3_credentials_from_env(bucket_prefix):
return {
@@ -132,15 +121,14 @@ class Test(Development):
WTF_CSRF_ENABLED = False
ASSET_DOMAIN = "static.example.com"
ASSET_PATH = "https://static.example.com/"
API_HOST_NAME = "http://you-forgot-to-mock-an-api-call-to"
API_PUBLIC_URL = "http://you-forgot-to-mock-an-api-call-to"
API_PUBLIC_WS_URL = "ws://you-forgot-to-mock-an-api-call-to"
REDIS_URL = "redis://you-forgot-to-mock-a-redis-call-to"
LOGO_CDN_DOMAIN = "static-logos.test.com"
class Production(Config):
HEADER_COLOUR = "#005EA5" # primary blue
HEADER_COLOUR = "#005EA5" # $govuk-blue
HTTP_PROTOCOL = "https"
ASSET_DOMAIN = "" # TODO use a CDN
ASSET_PATH = "/static/" # TODO use a CDN

View File

@@ -5,7 +5,7 @@
Explore Notify, add team members, and practice [sending messages to teammates](/using-notify/trial-mode).
2. ## Personalize content
Learn how to [personalize messages](/using-notify/how-to) to increase response.
Learn how to [personalize messages](/using-notify/guidance) to increase response.
3. ## Check delivery status
[Analyze the delivery](/using-notify/delivery-status) of your messages and download reports
@@ -14,7 +14,7 @@ Learn how to [personalize messages](/using-notify/how-to) to increase response.
Learn about message _parts_ and [how limits are calculated](/using-notify/pricing).
5. ## Start sending messages
To remove the restrictions of Trial Mode and begin sending messages to clients complete the <a class="usa-link usa-link--external" href="https://docs.google.com/forms/d/1fnaBtxuGf3q-OdGVyt2LqBKvp9_P21kmKJa0yIK8rWM/edit">Go-Live Form</a>.
To remove the restrictions of Trial Mode and begin sending messages to clients complete the [Live Campaign Form](https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL).
Well respond within one business day.
### Questions?

View File

@@ -1,107 +0,0 @@
from enum import StrEnum
class NotificationStatus(StrEnum):
CREATED = "created"
PENDING = "pending"
SENDING = "sending"
DELIVERED = "delivered"
SENT = "sent"
FAILED = "failed"
TEMPORARY_FAILURE = "temporary-failure"
PERMANENT_FAILURE = "permanent-failure"
TECHNICAL_FAILURE = "technical-failure"
VALIDATION_FAILED = "validation-failed"
CANCELLED = "cancelled"
@classmethod
def sending_statuses(cls):
return [cls.CREATED, cls.PENDING, cls.SENDING]
@classmethod
def delivered_statuses(cls):
return [cls.DELIVERED, cls.SENT]
@classmethod
def failure_statuses(cls):
return [
cls.FAILED,
cls.TEMPORARY_FAILURE,
cls.PERMANENT_FAILURE,
cls.TECHNICAL_FAILURE,
cls.VALIDATION_FAILED,
]
@classmethod
def requested_statuses(cls):
return (
cls.sending_statuses() + cls.delivered_statuses() + cls.failure_statuses()
)
class NotificationType(StrEnum):
EMAIL = "email"
SMS = "sms"
class OrganizationType(StrEnum):
FEDERAL = "federal"
STATE = "state"
OTHER = "other"
class ApiKeyType(StrEnum):
NORMAL = "normal"
TEAM = "team"
TEST = "test"
class JobStatus(StrEnum):
PENDING = "pending"
IN_PROGRESS = "in progress"
FINISHED = "finished"
SENDING_LIMITS_EXCEEDED = "sending limits exceeded"
SCHEDULED = "scheduled"
CANCELLED = "cancelled"
READY_TO_SEND = "ready to send"
SENT_TO_DVLA = "sent to dvla"
class ServicePermission(StrEnum):
SEND_MESSAGES = "send_messages"
MANAGE_SERVICE = "manage_service"
MANAGE_TEMPLATES = "manage_templates"
SEND_TEXTS = "send_texts"
MANAGE_SETTINGS = "manage_settings"
INBOUND_SMS = "inbound_sms"
INTERNATIONAL_SMS = "international_sms"
EMAIL_AUTH = "email_auth"
EDIT_FOLDER_PERMISSIONS = "edit_folder_permissions"
RESEARCH_MODE = "research_mode"
UPLOAD_DOCUMENT = "upload_document"
VIEW_ACTIVITY = "view_activity"
MANAGE_USERS = "manage_users"
SEND_EMAILS = "send_emails"
class InvitedUserStatus(StrEnum):
ACCEPTED = "accepted"
CANCELLED = "cancelled"
EXPIRED = "expired"
class InvitedOrgUserStatus(StrEnum):
ACCEPTED = "accepted"
CANCELLED = "cancelled"
class VerificationStatus(StrEnum):
PENDING = "pending"
SUCCESS = "success"
class AuthType(StrEnum):
EMAIL_AUTH = "email_auth"
SMS_AUTH = "sms_auth"

View File

@@ -1,3 +1,5 @@
from notifications_utils.clients.redis.redis_client import RedisClient
from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient
zendesk_client = ZendeskClient()
redis_client = RedisClient()

View File

@@ -11,14 +11,13 @@ import ago
import dateutil
import humanize
import markdown
import pytz
from bs4 import BeautifulSoup
from flask import render_template_string, url_for
from flask import Markup, render_template_string, url_for
from flask.helpers import get_root_path
from markupsafe import Markup
from app.enums import AuthType, NotificationStatus
from app.utils.csv import get_user_preferred_timezone, get_user_preferred_timezone_obj
from app.utils.time import parse_naive_dt
from app.utils.csv import get_user_preferred_timezone
from app.utils.time import parse_dt, parse_naive_dt
from notifications_utils.field import Field
from notifications_utils.formatters import make_quotes_smart
from notifications_utils.formatters import nl2br as utils_nl2br
@@ -94,34 +93,16 @@ def format_datetime_normal(date):
)
def format_datetime_scheduled_notification(date):
# e.g. April 09, 2024 at 04:00 PM US/Eastern.
# Everything except scheduled notifications, the time is always "now".
# Scheduled notifications are the exception to the rule.
# Here we are formating and displaying the datetime without converting datetime to a different timezone.
datetime_obj = parse_naive_dt(date)
format_time_without_tz = datetime_obj.replace(tzinfo=timezone.utc).strftime(
"%I:%M %p"
)
return "{} at {} {}".format(
format_date_normal(date), format_time_without_tz, get_user_preferred_timezone()
)
def format_datetime_table(date):
# example: 03-18-2024 at 04:53 PM, intended for datetimes in tables
return "{} at {}".format(format_date_numeric(date), format_time_12h(date))
def format_time_12h(date):
date = parse_naive_dt(date)
date = parse_dt(date)
preferred_tz = get_user_preferred_timezone_obj()
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
)
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return date.astimezone(preferred_tz).strftime("%I:%M %p")
def format_datetime_relative(date):
@@ -139,7 +120,7 @@ def format_datetime_numeric(date):
def format_date_numeric(date):
date = parse_naive_dt(date)
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
)
@@ -148,14 +129,14 @@ def format_date_numeric(date):
def format_time_24h(date):
date = parse_naive_dt(date)
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%H:%M")
def get_human_day(time, date_prefix=""):
# Add 1 minute to transform 00:00 into midnight today instead of midnight tomorrow
time = parse_naive_dt(time)
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
time = time.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
date = (time - timedelta(minutes=1)).date()
now = datetime.now(preferred_tz)
@@ -180,7 +161,7 @@ def get_human_day(time, date_prefix=""):
def format_date(date):
date = parse_naive_dt(date)
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
date.replace(tzinfo=timezone.utc)
.astimezone(preferred_tz)
@@ -195,7 +176,7 @@ def format_date_normal(date):
def format_date_short(date):
date = parse_naive_dt(date)
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return _format_datetime_short(
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
)
@@ -215,7 +196,7 @@ def format_datetime_human(date, date_prefix=""):
def format_day_of_week(date):
date = parse_naive_dt(date)
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%A")
@@ -231,11 +212,6 @@ def naturaltime_without_indefinite_article(date):
)
def convert_time_unixtimestamp(date_string):
dt = datetime.fromisoformat(date_string)
return int(dt.timestamp())
def format_delta(date):
# This method assumes that date is in UTC
date = parse_naive_dt(date)
@@ -276,53 +252,46 @@ def format_notification_type(notification_type):
def format_notification_status(status, template_type):
return {
"email": {
NotificationStatus.FAILED: "Failed",
NotificationStatus.TECHNICAL_FAILURE: "Technical failure",
NotificationStatus.TEMPORARY_FAILURE: "Inbox not accepting messages right now",
NotificationStatus.PERMANENT_FAILURE: "Email address does not exist",
NotificationStatus.DELIVERED: "Delivered",
NotificationStatus.SENDING: "Sending",
NotificationStatus.CREATED: "Sending",
NotificationStatus.SENT: "Delivered",
"failed": "Failed",
"technical-failure": "Technical failure",
"temporary-failure": "Inbox not accepting messages right now",
"permanent-failure": "Email address does not exist",
"delivered": "Delivered",
"sending": "Sending",
"created": "Sending",
"sent": "Delivered",
},
"sms": {
NotificationStatus.FAILED: "Failed",
NotificationStatus.TECHNICAL_FAILURE: "Technical failure",
NotificationStatus.TEMPORARY_FAILURE: "Phone not accepting messages right now",
NotificationStatus.PERMANENT_FAILURE: "Not delivered",
NotificationStatus.DELIVERED: "Delivered",
NotificationStatus.SENDING: "Sending",
NotificationStatus.CREATED: "Sending",
NotificationStatus.PENDING: "Sending",
NotificationStatus.SENT: "Sent",
"failed": "Failed",
"technical-failure": "Technical failure",
"temporary-failure": "Phone not accepting messages right now",
"permanent-failure": "Not delivered",
"delivered": "Delivered",
"sending": "Sending",
"created": "Sending",
"pending": "Sending",
"sent": "Sent",
},
}[template_type].get(status, status)
def format_notification_status_as_time(status, created, updated):
return dict.fromkeys(
{
NotificationStatus.CREATED,
NotificationStatus.PENDING,
NotificationStatus.SENDING,
},
" since {}".format(created),
{"created", "pending", "sending"}, " since {}".format(created)
).get(status, updated)
def format_notification_status_as_field_status(status, notification_type):
return {
NotificationStatus.FAILED: "error",
NotificationStatus.TECHNICAL_FAILURE: "error",
NotificationStatus.TEMPORARY_FAILURE: "error",
NotificationStatus.PERMANENT_FAILURE: "error",
NotificationStatus.DELIVERED: None,
NotificationStatus.SENT: (
"sent-international" if notification_type == "sms" else None
),
NotificationStatus.SENDING: "default",
NotificationStatus.CREATED: "default",
NotificationStatus.PENDING: "default",
"failed": "error",
"technical-failure": "error",
"temporary-failure": "error",
"permanent-failure": "error",
"delivered": None,
"sent": "sent-international" if notification_type == "sms" else None,
"sending": "default",
"created": "default",
"pending": "default",
}.get(status, "error")
@@ -330,9 +299,9 @@ def format_notification_status_as_url(status, notification_type):
url = partial(url_for, "main.message_status")
if status not in {
NotificationStatus.TECHNICAL_FAILURE,
NotificationStatus.TEMPORARY_FAILURE,
NotificationStatus.PERMANENT_FAILURE,
"technical-failure",
"temporary-failure",
"permanent-failure",
}:
return None
@@ -351,7 +320,7 @@ def nl2br(value):
html="escape",
)
).then(utils_nl2br)
) # nosec
)
return ""
@@ -421,7 +390,7 @@ def redact_mobile_number(mobile_number, spacing=""):
return "".join(mobile_number_list)
def get_time_left(created_at, service_data_retention_days=8):
def get_time_left(created_at, service_data_retention_days=7):
return ago.human(
(datetime.now(timezone.utc))
- (
@@ -566,8 +535,8 @@ def square_metres_to_square_miles(area):
def format_auth_type(auth_type, with_indefinite_article=False):
indefinite_article, auth_type = {
AuthType.EMAIL_AUTH: ("an", "Email link"),
AuthType.SMS_AUTH: ("a", "Text message code"),
"email_auth": ("an", "Email link"),
"sms_auth": ("a", "Text message code"),
}[auth_type]
if with_indefinite_article:

View File

@@ -3,7 +3,6 @@ from flask import Blueprint
main = Blueprint("main", __name__)
from app.main.views import ( # noqa isort:skip
activity,
add_service,
api_keys,
choose_account,

View File

@@ -3,13 +3,13 @@ from datetime import datetime, timedelta
from itertools import chain
from numbers import Number
from flask import render_template, request
import pytz
from flask import Markup, render_template, request
from flask_login import current_user
from flask_wtf import FlaskForm as Form
from flask_wtf.file import FileAllowed
from flask_wtf.file import FileField as FileField_wtf
from flask_wtf.file import FileSize
from markupsafe import Markup
from werkzeug.utils import cached_property
from wtforms import (
BooleanField,
@@ -49,7 +49,6 @@ from app.main.validators import (
CommonlyUsedPassword,
CsvFileValidator,
DoesNotStartWithDoubleZero,
FieldCannotContainComma,
LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly,
MustContainAlphanumericCharacters,
NoCommasInPlaceHolders,
@@ -61,7 +60,7 @@ from app.main.validators import (
)
from app.models.organization import Organization
from app.utils import merge_jsonlike
from app.utils.csv import get_user_preferred_timezone, get_user_preferred_timezone_obj
from app.utils.csv import get_user_preferred_timezone
from app.utils.user_permissions import all_ui_permissions, permission_options
from notifications_utils.formatters import strip_all_whitespace
from notifications_utils.insensitive_dict import InsensitiveDict
@@ -69,7 +68,7 @@ from notifications_utils.recipients import InvalidPhoneError, validate_phone_num
def get_time_value_and_label(future_time):
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (
future_time.astimezone(preferred_tz).isoformat(),
"{} at {} {}".format(
@@ -89,7 +88,7 @@ def get_human_time(time):
def get_human_day(time, prefix_today_with="T"):
# Add 1 hour to get midnight today instead of midnight tomorrow
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
time = (time - timedelta(hours=1)).strftime("%A")
if time == datetime.now(preferred_tz).strftime("%A"):
return "{}oday".format(prefix_today_with)
@@ -100,12 +99,12 @@ def get_human_day(time, prefix_today_with="T"):
def get_furthest_possible_scheduled_time():
# We want local time to find date boundaries
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
return (datetime.now(preferred_tz) + timedelta(days=4)).replace(hour=0)
def get_next_hours_until(until):
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
now = datetime.now(preferred_tz)
hours = int((until - now).total_seconds() / (60 * 60))
return [
@@ -115,7 +114,7 @@ def get_next_hours_until(until):
def get_next_days_until(until):
preferred_tz = get_user_preferred_timezone_obj()
preferred_tz = pytz.timezone(get_user_preferred_timezone())
now = datetime.now(preferred_tz)
days = int((until - now).total_seconds() / (60 * 60 * 24))
return [
@@ -256,7 +255,7 @@ def govuk_text_input_field_widget(
return Markup(
render_template("components/components/input/template.njk", params=params)
) # nosec
)
class GovukTextInputField(StringField):
@@ -654,7 +653,7 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
auth_type = HiddenField("auth_type", validators=[DataRequired()])
def uswds_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
# error messages
error_message = None
if field.errors:
@@ -689,12 +688,10 @@ def uswds_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
if param_extensions:
merge_jsonlike(params, param_extensions)
return Markup(
render_template("forms/fields/checkboxes/macro.njk", params=params)
) # nosec
return Markup(render_template("forms/fields/checkboxes/macro.njk", params=params))
def uswds_checkboxes_field_widget(
def govuk_checkboxes_field_widget(
self, field, wrap_in_collapsible=False, param_extensions=None, **kwargs
):
def _wrap_in_collapsible(field_label, checkboxes_string):
@@ -705,7 +702,7 @@ def uswds_checkboxes_field_widget(
f' data-field-label="{field_label}">'
f" {checkboxes_string}"
f"</div>"
) # nosec
)
return result
@@ -758,14 +755,12 @@ def uswds_checkboxes_field_widget(
return _wrap_in_collapsible(
self.field_label,
Markup(
render_template("forms/fields/checkboxes/macro.njk", params=params)
), # nosec
Markup(render_template("forms/fields/checkboxes/macro.njk", params=params)),
)
else:
return Markup(
render_template("forms/fields/checkboxes/macro.njk", params=params)
) # nosec
)
def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
@@ -808,12 +803,12 @@ def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
return Markup(
render_template("components/components/radios/template.njk", params=params)
) # nosec
)
class USWDSCheckboxField(BooleanField):
class GovukCheckboxField(BooleanField):
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(USWDSCheckboxField, self).__init__(
super(GovukCheckboxField, self).__init__(
label, validators, false_values=None, **kwargs
)
self.param_extensions = param_extensions
@@ -823,17 +818,17 @@ class USWDSCheckboxField(BooleanField):
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return uswds_checkbox_field_widget(
return govuk_checkbox_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
class USWDSCheckboxesField(SelectMultipleField):
class GovukCheckboxesField(SelectMultipleField):
render_as_list = False
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
super(USWDSCheckboxesField, self).__init__(label, validators, **kwargs)
super(GovukCheckboxesField, self).__init__(label, validators, **kwargs)
self.param_extensions = param_extensions
def get_item_from_option(self, option):
@@ -853,32 +848,32 @@ class USWDSCheckboxesField(SelectMultipleField):
# 2. calls field.widget
# this bypasses that by making self.widget a method with the same interface as widget.__call__
def widget(self, field, param_extensions=None, **kwargs):
return uswds_checkboxes_field_widget(
return govuk_checkboxes_field_widget(
self, field, param_extensions=param_extensions, **kwargs
)
# Wraps checkboxes rendering in HTML needed by the collapsible JS
class USWDSCollapsibleCheckboxesField(USWDSCheckboxesField):
class GovukCollapsibleCheckboxesField(GovukCheckboxesField):
def __init__(
self, label="", validators=None, field_label="", param_extensions=None, **kwargs
):
super(USWDSCollapsibleCheckboxesField, self).__init__(
super(GovukCollapsibleCheckboxesField, self).__init__(
label, validators, param_extensions, **kwargs
)
self.field_label = field_label
def widget(self, field, **kwargs):
return uswds_checkboxes_field_widget(
return govuk_checkboxes_field_widget(
self, field, wrap_in_collapsible=True, param_extensions=None, **kwargs
)
# USWDSCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
# GovukCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
# collapsible JS
# NestedFieldMixin puts the items into a tree hierarchy, pre-rendering the sub-trees of the top-level items
class USWDSCollapsibleNestedCheckboxesField(
NestedFieldMixin, USWDSCollapsibleCheckboxesField
class GovukCollapsibleNestedCheckboxesField(
NestedFieldMixin, GovukCollapsibleCheckboxesField
):
NONE_OPTION_VALUE = None
render_as_list = True
@@ -997,7 +992,7 @@ class BasePermissionsForm(StripWhitespaceForm):
for item in ([{"name": "Templates", "id": None}] + all_template_folders)
]
folder_permissions = USWDSCollapsibleNestedCheckboxesField(
folder_permissions = GovukCollapsibleNestedCheckboxesField(
"Folders this team member can see", field_label="folder"
)
@@ -1011,7 +1006,7 @@ class BasePermissionsForm(StripWhitespaceForm):
validators=[DataRequired()],
)
permissions_field = USWDSCheckboxesField(
permissions_field = GovukCheckboxesField(
"Permissions",
filters=[filter_by_permissions],
choices=[(value, label) for value, label in permission_options],
@@ -1421,7 +1416,7 @@ class ServiceContactDetailsForm(StripWhitespaceForm):
class ServiceReplyToEmailForm(StripWhitespaceForm):
email_address = email_address(label="Reply-to email address", gov_user=False)
is_default = USWDSCheckboxField("Make this email address the default")
is_default = GovukCheckboxField("Make this email address the default")
class ServiceSmsSenderForm(StripWhitespaceForm):
@@ -1435,11 +1430,11 @@ class ServiceSmsSenderForm(StripWhitespaceForm):
DoesNotStartWithDoubleZero(),
],
)
is_default = USWDSCheckboxField("Make this text message sender the default")
is_default = GovukCheckboxField("Make this text message sender the default")
class ServiceEditInboundNumberForm(StripWhitespaceForm):
is_default = USWDSCheckboxField("Make this text message sender the default")
is_default = GovukCheckboxField("Make this text message sender the default")
class AdminNotesForm(StripWhitespaceForm):
@@ -1527,7 +1522,7 @@ class GuestList(StripWhitespaceForm):
class DateFilterForm(StripWhitespaceForm):
start_date = GovukDateField("Start Date", [validators.optional()])
end_date = GovukDateField("End Date", [validators.optional()])
include_from_test_key = USWDSCheckboxField("Include test keys")
include_from_test_key = GovukCheckboxField("Include test keys")
class RequiredDateFilterForm(StripWhitespaceForm):
@@ -1654,11 +1649,7 @@ def get_placeholder_form_instance(
) # TODO: replace with us_mobile_number
else:
field = GovukTextInputField(
placeholder_name,
validators=[
DataRequired(message="Cannot be empty"),
FieldCannotContainComma(),
],
placeholder_name, validators=[DataRequired(message="Cannot be empty")]
)
PlaceholderForm.placeholder_value = field
@@ -1726,20 +1717,12 @@ class TemplateFolderForm(StripWhitespaceForm):
def __init__(self, all_service_users=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if all_service_users is not None:
regular_users = [
user for user in all_service_users if not user.platform_admin
]
platform_admins = [
user for user in all_service_users if user.platform_admin
]
self.users_with_permission.all_service_users = regular_users
self.users_with_permission.all_service_users = all_service_users
self.users_with_permission.choices = [
(item.id, item.name) for item in regular_users
(item.id, item.name) for item in all_service_users
]
self.platform_admins = platform_admins
users_with_permission = USWDSCollapsibleCheckboxesField(
users_with_permission = GovukCollapsibleCheckboxesField(
"Team members who can see this folder", field_label="team member"
)
name = GovukTextInputField(
@@ -1858,7 +1841,7 @@ class TemplateAndFoldersSelectionForm(Form):
return self.move_to_new_folder_name.data
return None
templates_and_folders = USWDSCheckboxesField(
templates_and_folders = GovukCheckboxesField(
"Choose templates or folders",
validators=[required_for_ops("move-to-new-folder", "move-to-existing-folder")],
choices=[], # added to keep order of arguments, added properly in __init__
@@ -1890,10 +1873,21 @@ class TemplateAndFoldersSelectionForm(Form):
class AdminClearCacheForm(StripWhitespaceForm):
model_type = USWDSCheckboxesField(
model_type = GovukCheckboxesField(
"What do you want to clear today",
)
def validate_model_type(self, field):
if not field.data:
raise ValidationError("Select at least one option")
class ChangeSecurityKeyNameForm(StripWhitespaceForm):
security_key_name = GovukTextInputField(
"Name of key",
validators=[
DataRequired(message="Cannot be empty"),
MustContainAlphanumericCharacters(),
Length(max=255, message="Name of key must be 255 characters or fewer"),
],
)

View File

@@ -41,8 +41,14 @@ class ValidGovEmail:
if field.data == "":
return
message = "Enter a public sector email address."
from flask import url_for
message = """
Enter a public sector email address or
<a class="usa-link" href="{}">find out who can use Notify</a>
""".format(
url_for("main.features")
)
if not is_gov_user(field.data.lower()):
raise ValidationError(message)
@@ -61,7 +67,7 @@ class ValidEmail:
class NoCommasInPlaceHolders:
def __init__(self, message="You cannot put commas between double parenthesis"):
def __init__(self, message="You cannot put commas between double brackets"):
self.message = message
def __call__(self, form, field):
@@ -108,17 +114,33 @@ class OnlySMSCharacters:
)
if non_sms_characters:
raise ValidationError(
"Please remove the unaccepted character {} in your message, then save again".format(
"You cannot use {} in {}. {} will not show up properly on everyones phones.".format(
formatted_list(
non_sms_characters,
conjunction="and",
conjunction="or",
before_each="",
after_each="",
),
{
"sms": "text messages",
}.get(self._template_type),
("It" if len(non_sms_characters) == 1 else "They"),
)
)
# class NoPlaceholders:
# def __init__(self, message=None):
# self.message = message or (
# 'You cant use ((double brackets)) to personalize this message'
# )
# def __call__(self, form, field):
# if Field(field.data).placeholders:
# raise ValidationError(self.message)
class LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly:
regex = re.compile(r"^[a-zA-Z0-9\s\._']+$")
@@ -139,15 +161,6 @@ class DoesNotStartWithDoubleZero:
raise ValidationError(self.message)
class FieldCannotContainComma:
def __init__(self, message="Cannot contain a comma"):
self.message = message
def __call__(self, form, field):
if field.data and "," in field.data:
raise ValidationError(self.message)
class MustContainAlphanumericCharacters:
regex = re.compile(r".*[a-zA-Z0-9].*[a-zA-Z0-9].*")

View File

@@ -1,215 +0,0 @@
import gevent
from flask import abort, render_template, request, url_for
from app import current_service, job_api_client
from app.enums import NotificationStatus, ServicePermission
from app.formatters import get_time_left
from app.main import main
from app.utils.pagination import (
generate_next_dict,
generate_pagination_pages,
generate_previous_dict,
get_page_from_request,
)
from app.utils.user import user_has_permissions
def get_report_info(service_id, report_name, s3_config):
try:
from app.s3_client import check_s3_file_exists, get_s3_object
from app.s3_client.s3_csv_client import NEW_FILE_LOCATION_STRUCTURE
key = NEW_FILE_LOCATION_STRUCTURE.format(service_id, report_name)
obj = get_s3_object(
s3_config["bucket"],
key,
s3_config["access_key_id"],
s3_config["secret_access_key"],
s3_config["region"],
)
exists = check_s3_file_exists(obj)
if exists:
# check_s3_file_exists already called obj.load(), so metadata should be populated
size_bytes = obj.content_length
# Only show as available if file has any content (not empty)
if size_bytes > 0:
if size_bytes < 1024:
size_str = f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
size_str = f"{size_bytes / 1024:.1f} KB"
else:
size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
return {"available": True, "size": size_str}
except Exception: # nosec B110
pass
return {"available": False, "size": None}
def get_download_availability(service_id):
from flask import current_app
# Get S3 config before spawning greenlets
s3_config = {
"bucket": current_app.config["CSV_UPLOAD_BUCKET"]["bucket"],
"access_key_id": current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"],
"secret_access_key": current_app.config["CSV_UPLOAD_BUCKET"][
"secret_access_key"
],
"region": current_app.config["CSV_UPLOAD_BUCKET"]["region"],
}
report_names = ["1-day-report", "3-day-report", "5-day-report", "7-day-report"]
greenlets = [
gevent.spawn(get_report_info, service_id, name, s3_config)
for name in report_names
]
gevent.joinall(greenlets)
results = [g.value for g in greenlets]
return {
"report_1_day": (
results[0] if results[0] else {"available": False, "size": None}
),
"report_3_day": (
results[1] if results[1] else {"available": False, "size": None}
),
"report_5_day": (
results[2] if results[2] else {"available": False, "size": None}
),
"report_7_day": (
results[3] if results[3] else {"available": False, "size": None}
),
}
def get_download_links(message_type):
time_periods = ["one_day", "three_day", "five_day", "seven_day"]
links = {}
for period in time_periods:
links[f"download_link_{period}"] = url_for(
".download_notifications_csv",
service_id=current_service.id,
message_type=message_type,
status=request.args.get("status"),
number_of_days=period,
)
return links
def get_filtered_jobs(service_id, page):
filter_type = request.args.get("filter")
limit_days = None
if filter_type == "24hours":
limit_days = 1
elif filter_type == "3days":
limit_days = 3
elif filter_type == "7days":
limit_days = 7
if limit_days:
return job_api_client.get_page_of_jobs(
service_id, page=page, limit_days=limit_days, use_processing_time=True
)
else:
return job_api_client.get_page_of_jobs(service_id, page=page)
@main.route("/activity/services/<uuid:service_id>")
@user_has_permissions(ServicePermission.VIEW_ACTIVITY)
def all_jobs_activity(service_id):
service_data_retention_days = 8
page = get_page_from_request()
jobs = get_filtered_jobs(service_id, page)
all_jobs_dict = generate_job_dict(jobs)
prev_page, next_page, pagination = handle_pagination(jobs, service_id, page)
message_type = ("sms",)
download_availability = get_download_availability(service_id)
download_links = get_download_links(message_type)
return render_template(
"views/activity/all-activity.html",
all_jobs_dict=all_jobs_dict,
service_data_retention_days=service_data_retention_days,
next_page=next_page,
prev_page=prev_page,
pagination=pagination,
total_jobs=jobs.get("total", 0),
**download_availability,
**download_links,
)
def handle_pagination(jobs, service_id, page):
if page is None:
abort(404, "Invalid page argument ({}).".format(request.args.get("page")))
url_args = {}
if request.args.get("filter"):
url_args["filter"] = request.args.get("filter")
prev_page = (
generate_previous_dict("main.all_jobs_activity", service_id, page, url_args)
if page > 1
else None
)
total_items = jobs.get("total", 0)
page_size = jobs.get("page_size", 50)
total_pages = (total_items + page_size - 1) // page_size
has_next_link = jobs.get("links", {}).get("next") is not None
next_page = (
generate_next_dict("main.all_jobs_activity", service_id, page, url_args)
if has_next_link and total_items > 50 and page < total_pages
else None
)
pagination = generate_pagination_pages(
jobs.get("total", {}), jobs.get("page_size", {}), page
)
return prev_page, next_page, pagination
def get_job_statistics(job, status):
statistics = job.get("statistics", [])
for stat in statistics:
if stat.get("status") == status:
return stat.get("count")
return None
def create_job_dict_entry(job):
job_id = job.get("id")
can_download = get_time_left(job.get("created_at")) != "Data no longer available"
activity_time = job.get("processing_started") or job.get("created_at")
return {
"job_id": job_id,
"can_download": can_download,
"download_link": (
url_for(".view_job_csv", service_id=current_service.id, job_id=job_id)
if can_download
else None
),
"view_job_link": url_for(
".view_job", service_id=current_service.id, job_id=job_id
),
"activity_time": activity_time,
"created_by": job.get("created_by"),
"template_name": job.get("template_name"),
"delivered_count": get_job_statistics(job, NotificationStatus.DELIVERED),
"failed_count": get_job_statistics(job, NotificationStatus.FAILED),
}
def generate_job_dict(jobs):
if not jobs or not jobs.get("data"):
return []
return [create_job_dict_entry(job) for job in jobs["data"]]

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