Compare commits
1 Commits
save_csv
...
807-clarif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc6e1c5265 |
688
.ds.baseline
@@ -1,688 +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": 121,
|
||||
"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
|
||||
}
|
||||
],
|
||||
"app/templates/new/components/head.html": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/templates/new/components/head.html",
|
||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||
"is_verified": false,
|
||||
"line_number": 33,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/templates/old/admin_template.html": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/templates/old/admin_template.html",
|
||||
"hashed_secret": "ee5048791fc7ff45a1545e24f85bec3317371327",
|
||||
"is_verified": false,
|
||||
"line_number": 18,
|
||||
"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": 388,
|
||||
"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": 33,
|
||||
"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": 643,
|
||||
"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": 122,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/main/views/test_register.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 201,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/main/views/test_register.py",
|
||||
"hashed_secret": "bb5b7caa27d005d38039e3797c3ddb9bcd22c3c8",
|
||||
"is_verified": false,
|
||||
"line_number": 274,
|
||||
"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": 97,
|
||||
"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": 55,
|
||||
"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": 3266,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py",
|
||||
"hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/encryption/test_encryption_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py",
|
||||
"hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605",
|
||||
"is_verified": false,
|
||||
"line_number": 13,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py",
|
||||
"hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2024-10-29T19:28:03Z"
|
||||
}
|
||||
71
.github/ISSUE_TEMPLATE/bug_template.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: "Bug Report"
|
||||
description: "Use this template to file a bug report"
|
||||
title: "Bug: [Brief description of the bug]"
|
||||
|
||||
labels:
|
||||
- "Type: Bug"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please use this form to file new bugs! Add screenshots to the comments of this initial form.
|
||||
- type: input
|
||||
id: contact
|
||||
attributes:
|
||||
label: Who discovered this?
|
||||
description: Was it something you found or are you filing for someone else? If someone else, how can we contact them?
|
||||
placeholder: ex. email@example.com
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: In which environment did you experience the bug?
|
||||
options:
|
||||
- staging
|
||||
- demo
|
||||
- production
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: page
|
||||
attributes:
|
||||
label: What pages did this appear on?
|
||||
description: please include URLs of where you saw the bug
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproSteps
|
||||
attributes:
|
||||
label: "Detail the steps for someone to reproduce"
|
||||
description: "Reproduction steps"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
76
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
@@ -1,76 +0,0 @@
|
||||
name: "User Story Template"
|
||||
description: "Use this template for creating user stories"
|
||||
title: "User Story: [Brief description of the user story]"
|
||||
|
||||
labels:
|
||||
- "Type: User Story"
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**User Story:**'
|
||||
- type: textarea
|
||||
id: userType
|
||||
attributes:
|
||||
label: "As a [type of user],"
|
||||
description: "Describe the type of user involved in this story."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actionFeature
|
||||
attributes:
|
||||
label: "I want [an action or feature],"
|
||||
description: "Describe the desired action or feature from the user's perspective."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: benefitValue
|
||||
attributes:
|
||||
label: "So that [benefit or value]."
|
||||
description: "Describe the benefit or value the user expects from the action or feature."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Acceptance Criteria:**'
|
||||
- type: textarea
|
||||
id: acceptanceCriteria
|
||||
attributes:
|
||||
label: "Detailed condition or criteria that must be met for the user story to be considered complete."
|
||||
description: "List the acceptance criteria for the user story."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Tasks:**'
|
||||
- type: textarea
|
||||
id: tasks
|
||||
attributes:
|
||||
label: "List of specific tasks or sub-tasks that need to be done to implement the user story."
|
||||
description: "Outline the tasks necessary to implement the user story."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Dependencies:**'
|
||||
- type: textarea
|
||||
id: dependencies
|
||||
attributes:
|
||||
label: "List any dependencies that need to be resolved before starting or completing this user story."
|
||||
description: "Specify any dependencies related to the user story."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '**Notes:**'
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: "Any additional information or context that might be useful for the team."
|
||||
description: "Provide any extra notes or context for the user story."
|
||||
validations:
|
||||
required: false
|
||||
82
.github/ISSUE_TEMPLATE/manual_qa_template.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: "QA Checklist"
|
||||
description: "Use this template to manually QA for prod push"
|
||||
title: "QA for prod push: Admin #[insert PR number], API #[insert PR number], Utils #[insert PR number]"
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: Logging_in
|
||||
attributes:
|
||||
label: Log In
|
||||
options:
|
||||
- label: notify-staging.app.cloud.gov resolves
|
||||
- label: Clicking sign in takes you to login page
|
||||
- label: 2FA code is sent promptly
|
||||
- label: 2FA code input screen appears as expected
|
||||
- label: After code is entered, you are brought to the “choose service” screen
|
||||
- label: Choosing a service brings you to the dashboard for that service
|
||||
|
||||
- type: checkboxes
|
||||
id: send_template
|
||||
attributes:
|
||||
label: Create a template and send
|
||||
options:
|
||||
- label: Clicking “Send messages” brings you to templates page
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Uploading a csv is quick and automatically directs you to the preview/send page
|
||||
- label: Formatting looks clean and in place
|
||||
- label: Message dashboard populates and shows “pending” until call is made to AWS for delivery receipt
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__copied_template
|
||||
attributes:
|
||||
label: Send a message from copied template
|
||||
options:
|
||||
- label: When “Create a template” button is clicked, the option to create new or copy existing template appears
|
||||
- label: Clicking “copy an existing template” allows you to choose from pre-existing template
|
||||
- label: After editing, the new template saves and you prepare to send
|
||||
|
||||
|
||||
- type: checkboxes
|
||||
id: send__existing_template
|
||||
attributes:
|
||||
label: Send a message from existing template
|
||||
options:
|
||||
- label: Clicking on existing template brings you to preview screen with `prepare to send` button
|
||||
- label: All else above persists as true (formatting, send, dashboard, etc)
|
||||
|
||||
- type: checkboxes
|
||||
id: download_report
|
||||
attributes:
|
||||
label: Download the csv delivery report
|
||||
options:
|
||||
- label: Clicking on the csv download directly after a message send downloads a report that contains the data we expect
|
||||
- label: The batch report appears on the main dashboard batch table with accurate information
|
||||
- label: Clicking on the csv download link from the main dashboard downloads a report that contains the data we expect
|
||||
|
||||
- type: checkboxes
|
||||
id: invite_member
|
||||
attributes:
|
||||
label: Invite a team member
|
||||
options:
|
||||
- label: Clicking on “Team” in settings brings you to team member screen
|
||||
- label: Clicking “Invite a team member” brings you to email and permissions screen
|
||||
- label: Clicking invite team member sends an invite and shows confirmation
|
||||
- label: Invite email provides live link that works to register new team member
|
||||
|
||||
- type: checkboxes
|
||||
id: delete_member
|
||||
attributes:
|
||||
label: Delete a team member
|
||||
options:
|
||||
- label: From team member screen, clicking “Change Details” on a specific profile brings you to that person’s profile
|
||||
- label: Clicking on “Remove this team member” prompts a confirmation
|
||||
- label: Clicking the confirmation removes the team member
|
||||
|
||||
- type: checkboxes
|
||||
id: sign_out
|
||||
attributes:
|
||||
label: Sign Out
|
||||
options:
|
||||
- label: Hitting “Sign Out” takes you back to the homepage and logs you out
|
||||
|
||||
16
.github/actions/deploy-proxy/action.yml
vendored
@@ -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 }}
|
||||
|
||||
6
.github/actions/setup-project/action.yml
vendored
@@ -9,10 +9,10 @@ runs:
|
||||
sudo apt-get update \
|
||||
&& sudo apt-get install -y --no-install-recommends \
|
||||
libcurl4-openssl-dev
|
||||
- name: Set up Python 3.12.3
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.12.3"
|
||||
python-version: "3.9"
|
||||
- name: Install poetry
|
||||
shell: bash
|
||||
run: pip install poetry
|
||||
|
||||
18
.github/dependabot.yml
vendored
@@ -3,22 +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
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "A-Shumway42"
|
||||
reviewers:
|
||||
- "A-Shumway42"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: "daily"
|
||||
versioning-strategy: increase
|
||||
assignees:
|
||||
- "alexjanousekGSA"
|
||||
reviewers:
|
||||
- "alexjanousekGSA"
|
||||
interval: 'daily'
|
||||
|
||||
|
||||
22
.github/pull_request_template.md
vendored
@@ -1,22 +0,0 @@
|
||||
<!--
|
||||
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)
|
||||
-->
|
||||
|
||||
*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.
|
||||
|
||||
## TODO (optional)
|
||||
|
||||
* [ ] TODO item 1
|
||||
* [ ] TODO item 2
|
||||
* [ ] TODO item ...
|
||||
|
||||
## Security Considerations
|
||||
|
||||
* Consideration 1
|
||||
* Consideration 2
|
||||
* Consideration ...
|
||||
125
.github/workflows/checks.yml
vendored
@@ -7,12 +7,12 @@ permissions:
|
||||
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: test
|
||||
NEW_RELIC_CONFIG_FILE: newrelic.ini
|
||||
NEW_RELIC_ENVIRONMENT: test
|
||||
FLASK_APP: application.py
|
||||
WERKZEUG_DEBUG_PIN: off
|
||||
REDIS_ENABLED: 0
|
||||
NODE_VERSION: 16.15.1
|
||||
AWS_US_TOLL_FREE_NUMBER: "+18556438890"
|
||||
ADMIN_BASE_URL: http://localhost:6012
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -22,13 +22,7 @@ jobs:
|
||||
contents: write
|
||||
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: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
@@ -44,89 +38,44 @@ jobs:
|
||||
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
|
||||
run: poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
# - name: Run E2E tests
|
||||
# run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
|
||||
# env:
|
||||
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
# NOTIFY_E2E_TEST_URI: ${{ secrets.NOTIFY_E2E_TEST_URI }}
|
||||
- name: Check coverage threshold
|
||||
run: poetry run coverage report --fail-under=90
|
||||
# - name: Health check
|
||||
# run: |
|
||||
# response=$(curl -url ${{secrets.NOTIFY_E2E_TEST_URI}}_status)
|
||||
# if grep -q "ok" <<< "$response"; then
|
||||
# echo "Health check passed"
|
||||
# else
|
||||
# echo "Health check failed"
|
||||
# exit 1
|
||||
# fi
|
||||
# env:
|
||||
# NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
# NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
# NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
# NOTIFY_E2E_TEST_URI: ${{ secrets.NOTIFY_E2E_TEST_URI }}
|
||||
|
||||
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
|
||||
env:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: test_notification_api
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
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 &
|
||||
tail -f admin-server.log &
|
||||
make e2e-test
|
||||
|
||||
env:
|
||||
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: notify-admin
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012/
|
||||
VCAP_SERVICES: ${{ secrets.VCAP_SERVICES }}
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Validate NewRelic config
|
||||
env:
|
||||
NEW_RELIC_CONFIG_FILE: newrelic.ini
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
# Need to set a NEW_RELIC_ENVIRONMENT with monitor_mode: true
|
||||
NEW_RELIC_ENVIRONMENT: staging
|
||||
@@ -135,22 +84,20 @@ jobs:
|
||||
dependency-audits:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
- uses: pypa/gh-action-pip-audit@v1.0.8
|
||||
- uses: pypa/gh-action-pip-audit@v1.0.6
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
PYSEC-2024-60
|
||||
- name: Run npm audit
|
||||
run: make npm-audit
|
||||
|
||||
static-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run scan
|
||||
run: poetry run bandit -r app/ --confidence-level medium
|
||||
@@ -158,7 +105,7 @@ jobs:
|
||||
dynamic-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run server
|
||||
run: make run-flask &
|
||||
@@ -167,17 +114,17 @@ jobs:
|
||||
- name: Run OWASP Baseline Scan
|
||||
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-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run server
|
||||
run: make run-flask &
|
||||
|
||||
95
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||
6
.github/workflows/daily_checks.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
dependency-audits:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
static-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run scan
|
||||
run: poetry run bandit -r app/ --confidence-level medium
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
dynamic-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-project
|
||||
- name: Run server
|
||||
run: make run-flask &
|
||||
|
||||
43
.github/workflows/deploy-demo.yml
vendored
@@ -12,17 +12,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: demo
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
with:
|
||||
files: |
|
||||
terraform/demo/**
|
||||
terraform/shared/**
|
||||
terraform/demo
|
||||
terraform/shared
|
||||
.github/workflows/deploy-demo.yml
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
@@ -47,53 +47,30 @@ jobs:
|
||||
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 }}
|
||||
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-demo.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-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"
|
||||
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_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"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-demo.*.acl
|
||||
@@ -102,10 +79,6 @@ jobs:
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
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-demo
|
||||
app: notify-admin-demo
|
||||
|
||||
43
.github/workflows/deploy-prod.yml
vendored
@@ -12,17 +12,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
with:
|
||||
files: |
|
||||
terraform/production/**
|
||||
terraform/shared/**
|
||||
terraform/production
|
||||
terraform/shared
|
||||
.github/workflows/deploy-prod.yml
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
@@ -47,53 +47,30 @@ jobs:
|
||||
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 }}
|
||||
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://beta.notify.gov/sign-out"
|
||||
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"
|
||||
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_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"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-production.*.acl
|
||||
@@ -102,10 +79,6 @@ jobs:
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
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-production
|
||||
app: notify-admin-production
|
||||
|
||||
159
.github/workflows/deploy.yml
vendored
@@ -17,105 +17,76 @@ jobs:
|
||||
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
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: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v1.1.2
|
||||
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
|
||||
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: ./.github/actions/setup-project
|
||||
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
- name: Create requirements.txt
|
||||
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
|
||||
- name: Deploy to cloud.gov
|
||||
uses: 18f/cg-deploy-action@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
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"
|
||||
|
||||
- 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"
|
||||
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"
|
||||
--strategy rolling
|
||||
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-staging.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
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@v34
|
||||
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
|
||||
|
||||
6
.github/workflows/drift.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check for drift
|
||||
uses: dflook/terraform-check@v1
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
environment: demo
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'production'
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'production'
|
||||
|
||||
|
||||
2
.github/workflows/terraform-demo.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
environment: demo
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
|
||||
2
.github/workflows/terraform-production.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
|
||||
2
.github/workflows/terraform-staging.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Terraform format
|
||||
id: format
|
||||
|
||||
14
.gitignore
vendored
@@ -14,7 +14,6 @@
|
||||
*.XLSX
|
||||
|
||||
## Non user files allowed to be commited
|
||||
!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
|
||||
@@ -124,19 +123,6 @@ app/templates/vendor
|
||||
secrets.auto.tfvars
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
requirements.txt
|
||||
terraform/sandbox/requirements.txt
|
||||
|
||||
# Playwright
|
||||
playwright/
|
||||
|
||||
# Pyenv
|
||||
.python-version
|
||||
|
||||
# Nodenv
|
||||
.node-version
|
||||
|
||||
# BackstopJS
|
||||
backstop_data/bitmaps_test/
|
||||
backstop_data/html_report/
|
||||
backstop_data/engine_scripts/playwright/
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: debug-statements
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- 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']
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
||||
3
.profile
@@ -4,6 +4,3 @@
|
||||
##
|
||||
|
||||
export NEW_RELIC_PROXY_HOST=$egress_proxy
|
||||
export http_proxy=$egress_proxy
|
||||
export https_proxy=$egress_proxy
|
||||
export no_proxy="apps.internal"
|
||||
|
||||
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.9
|
||||
31
Makefile
@@ -15,14 +15,9 @@ NVMSH := $(shell [ -f "$(HOME)/.nvm/nvm.sh" ] && echo "$(HOME)/.nvm/nvm.sh" || e
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: generate-version-file ## Set up everything to run the app
|
||||
poetry self update
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
poetry install
|
||||
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) --no-use && nvm install && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: watch-frontend
|
||||
@@ -41,10 +36,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}'
|
||||
@@ -76,19 +67,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 run --omit=*/notifications_utils/* -m pytest --maxfail=10 --ignore=tests/end_to_end tests/
|
||||
poetry run coverage report --fail-under=96
|
||||
poetry run coverage html -d .coverage_cache
|
||||
|
||||
.PHONY: dead-code
|
||||
dead-code: ## 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
|
||||
DEBUG=pw:api,pw:browser poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
|
||||
e2e-test: ## Run end-to-end integration tests
|
||||
poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
|
||||
|
||||
.PHONY: js-lint
|
||||
js-lint: ## Run javascript linting scanners
|
||||
@@ -102,11 +92,6 @@ js-test: ## Run javascript unit tests
|
||||
fix-imports: ## Fix imports using isort
|
||||
poetry run isort ./app ./tests
|
||||
|
||||
.PHONY: py-lock
|
||||
py-lock: ## Syncs dependencies and updates lock file without performing recursive internal updates
|
||||
poetry lock --no-update
|
||||
poetry install --sync
|
||||
|
||||
.PHONY: freeze-requirements
|
||||
freeze-requirements: ## create static requirements.txt
|
||||
poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||
@@ -116,7 +101,7 @@ pip-audit:
|
||||
poetry requirements > requirements.txt
|
||||
poetry requirements --dev > requirements_for_test.txt
|
||||
poetry run pip-audit -r requirements.txt
|
||||
poetry run pip-audit -r requirements_for_test.txt
|
||||
-poetry run pip-audit -r requirements_for_test.txt
|
||||
|
||||
.PHONY: audit
|
||||
audit: npm-audit pip-audit
|
||||
|
||||
518
README.md
@@ -1,10 +1,6 @@
|
||||

|
||||
# Notify UI
|
||||
|
||||
# Notify.gov Admin UI
|
||||
|
||||
This is the Notify front-end for government users and admins. To see it in
|
||||
action, check out [the demo site](https://notify-demo.app.cloud.gov)
|
||||
(contact team for credentials).
|
||||
This is the Notify front-end for government users and admins. To see it in action, check out [the demo site](https://notify-demo.app.cloud.gov) (contact team for credentials).
|
||||
|
||||
Through the interface, users can:
|
||||
|
||||
@@ -13,440 +9,69 @@ Through the interface, users can:
|
||||
- Send batch SMS by uploading a CSV
|
||||
- View their history of notifications
|
||||
|
||||
The [Notify.gov API](https://github.com/GSA/notifications-api) provides the
|
||||
UI's backend and is required for most things to function. Set that up first!
|
||||
The [Notify API](https://github.com/GSA/notifications-api) provides the UI's backend and is required for most things to function. Set that up first!
|
||||
|
||||
Our other repositories are:
|
||||
## Local setup
|
||||
|
||||
- [notifications-admin](https://github.com/GSA/notifications-admin)
|
||||
- [us-notify-compliance](https://github.com/GSA/us-notify-compliance/)
|
||||
- [notify-python-demo](https://github.com/GSA/notify-python-demo)
|
||||
### Common steps
|
||||
|
||||
## Before You Start
|
||||
1. Install pre-requisites for setup (on a Mac):
|
||||
- Install XCode or at least the XCode Command Line Tools
|
||||
- [Homebrew](https://brew.sh/) (follow instructions on page)
|
||||
- [jq](https://stedolan.github.io/jq/): `brew install jq`
|
||||
- [terraform](https://www.terraform.io/): `brew install terraform` or `brew install tfenv` and use `tfenv` to install `terraform ~> 1.4.0`
|
||||
- [cf-cli@8](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html): `brew install cloudfoundry/tap/cf-cli@8`
|
||||
1. [Log into cloud.gov](https://cloud.gov/docs/getting-started/setup/#set-up-the-command-line): `cf login -a api.fr.cloud.gov --sso`
|
||||
1. Ensure you have access to the `notify-local-dev` and `notify-staging` spaces in cloud.gov
|
||||
1. Run the API setup steps
|
||||
1. Run the development terraform with:
|
||||
|
||||
You will need the following items:
|
||||
```
|
||||
$ cd terraform/development
|
||||
$ ./run.sh
|
||||
```
|
||||
|
||||
- An active cloud.gov account with the correct permissions - speak with your
|
||||
onboarding buddy for help with
|
||||
[setting up an account](https://cloud.gov/sign-up/) (requires a `.mil`,
|
||||
`.gov`, or `.fed.us` email address) and getting access to the
|
||||
`notify-local-dev` and `notify-staging` spaces.
|
||||
- Admin priviliges and SSH access on your machine; you may need to work with
|
||||
your organization's IT support staff if you're not sure or don't currently
|
||||
have this access.
|
||||
1. If you want to send data to New Relic from your local develpment environment, set `NEW_RELIC_LICENSE_KEY` within `.env`
|
||||
1. Follow the instructions for either `Direct installation` or `Docker installation` below
|
||||
|
||||
**In addition, you should set up the Notify.gov API first!**
|
||||
### Direct installation
|
||||
|
||||
[Follow the instructions here to set up the Notify.gov API.](https://github.com/GSA/notifications-api#before-you-start)
|
||||
1. Get the API running
|
||||
|
||||
The Notify.gov API is required in order for the Notify.gov Admin UI to run, and
|
||||
it will also take care of many of the steps that are listed here. The sections
|
||||
that are a repeat from the API setup are flagged with an **[API Step]** label
|
||||
in front of them.
|
||||
1. Install [poetry](https://python-poetry.org/docs/#installation)
|
||||
|
||||
## Local Environment Setup
|
||||
1. Install [nvm (Node Version Manager)](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
|
||||
This project is set up to work with
|
||||
[nvm (Node Version Manager)](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
for managing and using Node.js (version 22.3.0) and the `npm` package manager.
|
||||
1. Install Python and Node dependencies
|
||||
|
||||
These instructions will walk you through how to set your machine up with all of
|
||||
the required tools for this project.
|
||||
`make bootstrap`
|
||||
|
||||
If you run into certificate errors at the `playwright install` step, try doing this:
|
||||
|
||||
1. Run `brew --prefix` to see Homebrew's root directory
|
||||
|
||||
1. Create or modify the local `.env` file in the project and add this line:
|
||||
|
||||
`NODE_EXTRA_CA_CERTS=/CHANGE-TO-HOMEBREW-INSTALL-PATH/etc/ca-certificates/cert.pem`
|
||||
|
||||
Make sure to change `CHANGE-TO-HOMEBREW-INSTALL-PATH` to the path given by `brew --prefix` in the step above.
|
||||
For example, if `brew --prefix` gave `/opt/homebrew` as output, then the line would look like this:
|
||||
|
||||
`NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem`
|
||||
|
||||
1. Save the changes to the `.env` file
|
||||
|
||||
1. Run `make bootstrap` again
|
||||
|
||||
1. Run the Flask server
|
||||
|
||||
`make run-flask`
|
||||
|
||||
1. Go to http://localhost:6012
|
||||
|
||||
### Docker installation
|
||||
|
||||
If you are using VS Code, there are also instructions for
|
||||
[running inside Docker](./docs/docker-remote-containers.md).
|
||||
|
||||
### [API Step] Project Pre-Requisite Setup
|
||||
|
||||
On MacOS, using [Homebrew](https://brew.sh/) for package management is highly
|
||||
recommended. This helps avoid some known installation issues. Start by following
|
||||
the installation instructions on the Homebrew homepage.
|
||||
|
||||
**Note:** You will also need Xcode or the Xcode Command Line Tools installed. The
|
||||
quickest way to do this is is by installing the command line tools in the shell:
|
||||
|
||||
```sh
|
||||
xcode-select –-install
|
||||
```
|
||||
|
||||
#### [API Step] Homebrew Setup
|
||||
|
||||
If this is your first time installing Homebrew on your machine, you may need to
|
||||
add its binaries to your system's `$PATH` environment variable so that you can
|
||||
use the `brew` command. Try running `brew help` to see if Homebrew is
|
||||
recognized and runs properly. If that fails, then you'll need to add a
|
||||
configuration line to wherever your `$PATH` environment variable is set.
|
||||
|
||||
Your system `$PATH` environment variable is likely set in one of these
|
||||
locations:
|
||||
|
||||
For BASH shells:
|
||||
|
||||
- `~/.bashrc`
|
||||
- `~/.bash_profile`
|
||||
- `~/.profile`
|
||||
|
||||
For ZSH shells:
|
||||
|
||||
- `~/.zshrc`
|
||||
- `~/.zprofile`
|
||||
|
||||
There may be different files that you need to modify for other shell
|
||||
environments.
|
||||
|
||||
Which file you need to modify depends on whether or not you are running an
|
||||
interactive shell or a login shell
|
||||
(see [this Stack Overflow post](https://stackoverflow.com/questions/18186929/what-are-the-differences-between-a-login-shell-and-interactive-shell)
|
||||
for an explanation of the differences). If you're still not sure, please ask
|
||||
the team for help!
|
||||
|
||||
Once you determine which file you'll need to modify, add these lines before any
|
||||
lines that add or modify the `$PATH` environment variable; near or at the top
|
||||
of the file is appropriate:
|
||||
|
||||
```sh
|
||||
# Homebrew setup
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
```
|
||||
|
||||
This will make sure Homebrew gets setup correctly. Once you make these changes,
|
||||
either start a new shell session or source the file
|
||||
(`source ~/.FILE-YOU-MODIFIED`) you modified to have your system recognize the
|
||||
changes.
|
||||
|
||||
Verify that Homebrew is now working by trying to run `brew help` again.
|
||||
|
||||
### [API Step] System-Level Package Installation
|
||||
|
||||
There are several packages you will need to install for your system in order to
|
||||
get the app running (and these are good to have in general for any software
|
||||
development).
|
||||
|
||||
Start off with these packages since they're quick and don't require additional
|
||||
configuration after installation to get working out of the box:
|
||||
|
||||
- [jq](https://stedolan.github.io/jq/) - for working with JSON in the command
|
||||
line
|
||||
- [git](https://git-scm.com/) - for version control management
|
||||
- [tfenv](https://github.com/tfutils/tfenv) - for managing
|
||||
[Terraform](https://www.terraform.io/) installations
|
||||
- [cf-cli@8](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html) - for
|
||||
working with a Cloud Foundry platform (e.g., cloud.gov)
|
||||
- [vim](https://www.vim.org/) - for editing files more easily in the command
|
||||
line
|
||||
- [wget](https://www.gnu.org/software/wget/) - for retrieving files in the
|
||||
command line
|
||||
|
||||
You can install them by running the following:
|
||||
|
||||
```sh
|
||||
brew install jq git tfenv cloudfoundry/tap/cf-cli@8 vim wget
|
||||
```
|
||||
|
||||
#### [API Step] Terraform Installation
|
||||
|
||||
As a part of the installation above, you just installed `tfenv` to manage
|
||||
Terraform installations. This is great, but you still need to install Terraform
|
||||
itself, which can be done with this command:
|
||||
|
||||
```sh
|
||||
tfenv install latest:^1.4.0
|
||||
```
|
||||
|
||||
_NOTE: This project currently uses the latest `1.4.x release of Terraform._
|
||||
|
||||
#### [API Step] Python Installation
|
||||
|
||||
Now we're going to install a tool to help us manage Python versions and
|
||||
virtual environments on our system. First, we'll install
|
||||
[pyenv](https://github.com/pyenv/pyenv) and one of its plugins,
|
||||
[pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv), with Homebrew:
|
||||
|
||||
```sh
|
||||
brew install pyenv pyenv-virtualenv
|
||||
```
|
||||
|
||||
When these finish installing, you'll need to make another adjustment in the
|
||||
file that you adjusted for your `$PATH` environment variable and Homebrew's
|
||||
setup. Open the file, and add these lines to it:
|
||||
|
||||
```
|
||||
# pyenv setup
|
||||
export PYENV_ROOT="$HOME/.pyenv"
|
||||
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
|
||||
eval "$(pyenv init -)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
```
|
||||
|
||||
Once again, start a new shell session or source the file in your current shell
|
||||
session to make the changes take effect.
|
||||
|
||||
Now we're ready to install the Python version we need with `pyenv`, like so:
|
||||
|
||||
```sh
|
||||
pyenv install 3.12
|
||||
```
|
||||
|
||||
This will install the latest version of Python 3.12.
|
||||
|
||||
_NOTE: This project currently runs on Python 3.12.x._
|
||||
|
||||
#### [API Step] Python Dependency Installation
|
||||
|
||||
Lastly, we need to install the tool we use to manage Python dependencies within
|
||||
the project, which is [poetry](https://python-poetry.org/).
|
||||
|
||||
Visit the
|
||||
[official installer instructions page](https://python-poetry.org/docs/#installing-with-the-official-installer)
|
||||
and follow the steps to install Poetry directly with the script.
|
||||
|
||||
This will ensure `poetry` doesn't conflict with any project virtual environments
|
||||
and can update itself properly.
|
||||
|
||||
#### Node.js and npm Installation
|
||||
|
||||
The project will manage most of the Node.js pieces for you, but you will need to
|
||||
install
|
||||
[nvm (Node Version Manager)](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
in order for things to work.
|
||||
|
||||
Follow the steps in the installation instructions to get `nvm` installed with
|
||||
the install script they provide, and double check that the file that you
|
||||
adjusted for your `$PATH` environment variable and Homebrew's setup is properly
|
||||
updated with the `nvm` setup as well.
|
||||
|
||||
Open the file and check for the `nvm` setup lines; if they're not there, then
|
||||
add the lines found in the
|
||||
[nvm installation instructions](https://github.com/nvm-sh/nvm#install--update-script)
|
||||
to to the file after the `pyenv` section you created, e.g.:
|
||||
|
||||
```
|
||||
# nvm setup
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
|
||||
```
|
||||
|
||||
### First-Time Project Setup
|
||||
|
||||
Once all of pre-requisites for the project are installed and you have a
|
||||
cloud.gov account, you can now set up the admin project and get things running
|
||||
locally!
|
||||
|
||||
First, clone the repository in the directory of your choosing on your machine:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:GSA/notifications-admin.git
|
||||
```
|
||||
|
||||
Now go into the project directory (`notifications-admin` by default), create a
|
||||
virtual environment, and set the local Python version to point to the virtual
|
||||
environment (assumes version Python `3.12.2` is what is installed on your
|
||||
machine):
|
||||
|
||||
```sh
|
||||
cd notifications-admin
|
||||
pyenv virtualenv 3.12.2 notify-admin
|
||||
pyenv local notify-admin
|
||||
```
|
||||
|
||||
_If you're not sure which version of Python was installed with `pyenv`, you can check by running `pyenv versions` and it'll list everything available currently._
|
||||
|
||||
Now [log into cloud.gov](https://cloud.gov/docs/getting-started/setup/#set-up-the-command-line)
|
||||
in the command line by using this command:
|
||||
|
||||
```sh
|
||||
cf login -a api.fr.cloud.gov --sso
|
||||
```
|
||||
|
||||
_REMINDER: Ensure you have access to the `notify-local-dev` and `notify-staging` spaces in cloud.gov_
|
||||
|
||||
Now run the development Terraform setup by navigating to the development
|
||||
folder and running the script in it:
|
||||
|
||||
```sh
|
||||
cd terraform/development
|
||||
./run.sh
|
||||
```
|
||||
|
||||
In addition to some infrastructure setup, this will also create a local `.env`
|
||||
file for you in the project's root directory, which will include a handful of
|
||||
project-specific environment variables.
|
||||
|
||||
#### Upgrading Python in existing projects
|
||||
|
||||
If you're upgrading an existing project to a newer version of Python, you can
|
||||
follow these steps to get yourself up-to-date.
|
||||
|
||||
First, use `pyenv` to install the newer version of Python you'd like to use;
|
||||
we'll use `3.12` in our example here since we recently upgraded to this version:
|
||||
|
||||
```sh
|
||||
pyenv install 3.12
|
||||
```
|
||||
|
||||
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
|
||||
pyenv virtualenv-delete notify-admin
|
||||
```
|
||||
|
||||
Now, make sure you are in your project directory and recreate the same virtual
|
||||
environment with the newer version of Python you just installed:
|
||||
|
||||
```sh
|
||||
cd notifications-admin
|
||||
pyenv virtualenv 3.12.2 notify-admin
|
||||
pyenv local notify-admin
|
||||
```
|
||||
|
||||
At this point, proceed with the rest of the instructions here in the README and
|
||||
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`._
|
||||
|
||||
#### 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
|
||||
adjustment in it for things to run properly for the E2E tests.
|
||||
|
||||
Open the `.env` file and look for the `NODE_EXTRA_CA_CERTS` environment variable
|
||||
toward the top. You will need to modify its value to be the path to Homebrew's
|
||||
CA certificate file.
|
||||
|
||||
Run `brew --prefix` to see Homebrew's root directory, and use that value to
|
||||
update the `NODE_EXTRA_CA_CERTS` environment variable with the path. For
|
||||
example, if `brew --prefix` gave `/opt/homebrew` as output, then the line would
|
||||
look like this:
|
||||
|
||||
```
|
||||
NODE_EXTRA_CA_CERTS=/opt/homebrew/etc/ca-certificates/cert.pem
|
||||
```
|
||||
|
||||
Save this change to the `.env` file and you'll be squared away.
|
||||
|
||||
## Running the Project and Routine Maintenance
|
||||
|
||||
The first time you run the project you'll need to run the project setup from the
|
||||
root project directory:
|
||||
|
||||
```sh
|
||||
make bootstrap
|
||||
```
|
||||
|
||||
This command is handled by the `Makefile` file in the root project directory, as
|
||||
are a few others.
|
||||
|
||||
_NOTE: You'll want to occasionally run `make bootstrap` to keep your project up-to-date, especially when there are dependency updates._
|
||||
|
||||
Now you can run the web server and background workers for asynchronous jobs:
|
||||
|
||||
```sh
|
||||
make run-flask
|
||||
```
|
||||
|
||||
This will run the local development web server and make the admin site
|
||||
available at http://localhost:6012; remember to make sure that the Notify.gov
|
||||
API is running as well!
|
||||
|
||||
## 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 two terminals pointing to the api project and then run these commands in the
|
||||
respective terminals.
|
||||
|
||||
(Server 1)
|
||||
env ALLOW_EXPIRED_API_TOKEN=1 make run-flask
|
||||
|
||||
(Server 2)
|
||||
poetry run flask command create-admin-jwt | tail -n 1 | pbcopy
|
||||
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
|
||||
automate common tasks or easily-missed cleanup. It's installed as part of
|
||||
`make bootstrap` and is limited to this project's virtualenv.
|
||||
|
||||
To run the hooks in advance of a `git` operation, use
|
||||
`poetry run pre-commit run`. For running across the whole codebase (useful after
|
||||
adding a new hook), use `poetry run pre-commit run --all-files`.
|
||||
|
||||
The configuration is stored in `.pre-commit-config.yaml`. In that config, there
|
||||
are links to the repos from which the hooks are pulled, so hop through there if
|
||||
ou want a detailed description of what each one is doing.
|
||||
|
||||
We do not maintain any hooks in this repository.
|
||||
|
||||
## Python Dependency Management
|
||||
|
||||
We're using [`Poetry`](https://python-poetry.org/) for managing our Python
|
||||
dependencies and local virtual environments. When it comes to managing the
|
||||
Python dependencies, there are a couple of things to bear in mind.
|
||||
|
||||
For situations where you manually manipulate the `pyproject.toml` file, you
|
||||
should use the `make py-lock` command to sync the `poetry.lock` file. This will
|
||||
ensure that you don't inadvertently bring in other transitive dependency updates
|
||||
that have not been fully tested with the project yet.
|
||||
|
||||
If you're just trying to update a dependency to a newer (or the latest) version,
|
||||
you should let Poetry take care of that for you by running the following:
|
||||
|
||||
```sh
|
||||
poetry update <dependency> [<dependency>...]
|
||||
```
|
||||
|
||||
You can specify more than one dependency together. With this command, Poetry
|
||||
will do the following for you:
|
||||
|
||||
- Find the latest compatible version(s) of the specified dependency/dependencies
|
||||
- Install the new versions
|
||||
- Update and sync the `poetry.lock` file
|
||||
|
||||
In either situation, once you are finished and have verified the dependency
|
||||
changes are working, please be sure to commit both the `pyproject.toml` and
|
||||
`poetry.lock` files.
|
||||
|
||||
## Known Installation Issues
|
||||
|
||||
### Python Installation Errors
|
||||
|
||||
On M1 Macs, if you get a `fatal error: 'Python.h' file not found` message, try a
|
||||
different method of installing Python. The recommended approach is to use
|
||||
[`pyenv`](https://github.com/pyenv/pyenv), as noted above in the installation
|
||||
instructions.
|
||||
|
||||
If you're using PyCharm for Python development, we've noticed some quirkiness
|
||||
with the IDE and the interaction between Poetry and virtual environment
|
||||
management that could cause a variety of problems to come up during project
|
||||
setup and dependency management. Other tools, such as Visual Studio Code, have
|
||||
proven to be a smoother experience for folks.
|
||||
If you are using VS Code, there are also instructions for [running inside Docker](./docs/docker-remote-containers.md)
|
||||
|
||||
## To test the application
|
||||
|
||||
@@ -460,13 +85,11 @@ make test
|
||||
npm run test-watch
|
||||
```
|
||||
|
||||
To run a specific JavaScript test, you'll need to copy the full command from
|
||||
`package.json`.
|
||||
To run a specific JavaScript test, you'll need to copy the full command from `package.json`.
|
||||
|
||||
## Running a11y-scans locally
|
||||
|
||||
Unlike most of the tests and scans, pa11y-ci cannot currently be run from within
|
||||
the VSCode dev container.
|
||||
Unlike most of the tests and scans, pa11y-ci cannot currently be run from within the VSCode dev container.
|
||||
|
||||
1. `make run-flask` from within the devcontainer
|
||||
2. Run `make a11y-scan` from your host computer.
|
||||
@@ -479,41 +102,16 @@ the VSCode dev container.
|
||||
|
||||
## License && public domain
|
||||
|
||||
Work through
|
||||
[commit `543be77`](https://github.com/GSA/notifications-admin/commit/543be77776b64fddb6ba70fbb015ecd81a372478)
|
||||
is licensed by the UK government under the MIT license. Work after that commit
|
||||
is in the worldwide public domain. See [LICENSE.md](./LICENSE.md) for more
|
||||
information.
|
||||
Work through [commit `543be77`](https://github.com/GSA/notifications-admin/commit/543be77776b64fddb6ba70fbb015ecd81a372478) is licensed by the UK government under the MIT license. Work after that commit is in the worldwide public domain. See [LICENSE.md](./LICENSE.md) for more information.
|
||||
|
||||
## Contributing
|
||||
|
||||
As stated in [CONTRIBUTING.md](CONTRIBUTING.md), all contributions to this
|
||||
project will be released under the CC0 dedication. By submitting a pull request,
|
||||
you are agreeing to comply with this waiver of copyright interest.
|
||||
As stated in [CONTRIBUTING.md](CONTRIBUTING.md), all contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest.
|
||||
|
||||
## About the TTS Public Benefits Studio
|
||||
|
||||
The Public Benefits Studio is a team inside of
|
||||
[GSA’s Technology Transformation Services](https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services)
|
||||
(TTS), home to innovative programs like [18F](https://18f.gsa.gov/) and
|
||||
[Login.gov](https://login.gov). We collaborate with benefits programs to develop
|
||||
shared technology tools and best practices that reduce the burden of navigating
|
||||
government programs for low income individuals and families.
|
||||
The Public Benefits Studio is a team inside of [GSA’s Technology Transformation Services](https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services) (TTS), home to innovative programs like [18F](https://18f.gsa.gov/) and [Login.gov](https://login.gov). We collaborate with benefits programs to develop shared technology tools and best practices that reduce the burden of navigating government programs for low income individuals and families.
|
||||
|
||||
We’re a cross-functional team of technologists with specialized experience
|
||||
working across public benefits programs like Medicaid, SNAP, and unemployment
|
||||
insurance.
|
||||
We’re a cross-functional team of technologists with specialized experience working across public benefits programs like Medicaid, SNAP, and unemployment insurance.
|
||||
|
||||
For more information on what we're working on, the Notify tool, and how to get
|
||||
involved with our team,
|
||||
[see our flyer.](https://github.com/GSA/notifications-admin/blob/main/docs/notify-pilot-flyer.md)
|
||||
|
||||
## 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.
|
||||
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)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
Contact: mailto:notify-support@gsa.gov
|
||||
Expires: 2035-10-15T23:59:59Z
|
||||
116
app/__init__.py
@@ -21,8 +21,15 @@ from flask_login import LoginManager, current_user
|
||||
from flask_talisman import Talisman
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_wtf.csrf import CSRFError
|
||||
from govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
||||
from itsdangerous import BadSignature
|
||||
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 werkzeug.exceptions import HTTPException as WerkzeugHTTPException
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.local import LocalProxy
|
||||
@@ -30,9 +37,8 @@ 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_to_boolean,
|
||||
format_auth_type,
|
||||
format_billions,
|
||||
@@ -46,8 +52,7 @@ from app.formatters import (
|
||||
format_datetime_human,
|
||||
format_datetime_normal,
|
||||
format_datetime_relative,
|
||||
format_datetime_scheduled_notification,
|
||||
format_datetime_table,
|
||||
format_datetime_short,
|
||||
format_day_of_week,
|
||||
format_delta,
|
||||
format_delta_days,
|
||||
@@ -83,12 +88,12 @@ from app.navigation import (
|
||||
HeaderNavigation,
|
||||
MainNavigation,
|
||||
OrgNavigation,
|
||||
SecondaryNavigation,
|
||||
)
|
||||
from app.notify_client import InviteTokenError
|
||||
from app.notify_client.api_key_api_client import api_key_api_client
|
||||
from app.notify_client.billing_api_client import billing_api_client
|
||||
from app.notify_client.complaint_api_client import complaint_api_client
|
||||
from app.notify_client.email_branding_client import email_branding_client
|
||||
from app.notify_client.events_api_client import events_api_client
|
||||
from app.notify_client.inbound_number_client import inbound_number_client
|
||||
from app.notify_client.invite_api_client import invite_api_client
|
||||
@@ -100,25 +105,24 @@ from app.notify_client.performance_dashboard_api_client import (
|
||||
performance_dashboard_api_client,
|
||||
)
|
||||
from app.notify_client.platform_stats_api_client import platform_stats_api_client
|
||||
from app.notify_client.provider_client import provider_client
|
||||
from app.notify_client.service_api_client import service_api_client
|
||||
from app.notify_client.status_api_client import status_api_client
|
||||
from app.notify_client.template_folder_api_client import template_folder_api_client
|
||||
from app.notify_client.template_statistics_api_client import template_statistics_client
|
||||
from app.notify_client.upload_api_client import upload_api_client
|
||||
from app.notify_client.user_api_client import user_api_client
|
||||
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
|
||||
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
|
||||
from notifications_utils import logging, request_helper
|
||||
from notifications_utils.formatters import (
|
||||
formatted_list,
|
||||
get_lines_with_normalised_whitespace,
|
||||
from app.url_converters import (
|
||||
SimpleDateTypeConverter,
|
||||
TemplateTypeConverter,
|
||||
TicketTypeConverter,
|
||||
)
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
|
||||
login_manager = LoginManager()
|
||||
csrf = CSRFProtect()
|
||||
talisman = Talisman()
|
||||
|
||||
|
||||
# The current service attached to the request stack.
|
||||
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
||||
|
||||
@@ -130,7 +134,6 @@ navigation = {
|
||||
"main_navigation": MainNavigation(),
|
||||
"header_navigation": HeaderNavigation(),
|
||||
"org_navigation": OrgNavigation(),
|
||||
"secondary_navigation": SecondaryNavigation(),
|
||||
}
|
||||
|
||||
|
||||
@@ -147,15 +150,8 @@ def _csp(config):
|
||||
"'unsafe-eval'",
|
||||
"https://js-agent.newrelic.com",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.googletagmanager.com",
|
||||
"https://www.google-analytics.com",
|
||||
"https://dap.digitalgov.gov",
|
||||
],
|
||||
"connect-src": [
|
||||
"'self'",
|
||||
"https://gov-bam.nr-data.net",
|
||||
"https://www.google-analytics.com",
|
||||
],
|
||||
"connect-src": ["'self'", "https://gov-bam.nr-data.net"],
|
||||
"style-src": ["'self'", asset_domain],
|
||||
"img-src": ["'self'", asset_domain, logo_domain],
|
||||
}
|
||||
@@ -184,6 +180,7 @@ def create_app(application):
|
||||
api_key_api_client,
|
||||
billing_api_client,
|
||||
complaint_api_client,
|
||||
email_branding_client,
|
||||
events_api_client,
|
||||
inbound_number_client,
|
||||
invite_api_client,
|
||||
@@ -193,6 +190,7 @@ def create_app(application):
|
||||
organizations_client,
|
||||
performance_dashboard_api_client,
|
||||
platform_stats_api_client,
|
||||
provider_client,
|
||||
service_api_client,
|
||||
status_api_client,
|
||||
template_folder_api_client,
|
||||
@@ -201,6 +199,7 @@ def create_app(application):
|
||||
user_api_client,
|
||||
# External API clients
|
||||
redis_client,
|
||||
zendesk_client,
|
||||
):
|
||||
client.init_app(application)
|
||||
|
||||
@@ -229,24 +228,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
|
||||
@@ -303,28 +284,13 @@ def init_app(application):
|
||||
|
||||
@application.context_processor
|
||||
def _attach_current_global_daily_messages():
|
||||
global_limit = 0
|
||||
remaining_global_messages = 0
|
||||
if current_app:
|
||||
if request.view_args:
|
||||
service_id = request.view_args.get(
|
||||
"service_id", session.get("service_id")
|
||||
)
|
||||
else:
|
||||
service_id = session.get("service_id")
|
||||
|
||||
if service_id:
|
||||
global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"]
|
||||
global_messages_count = (
|
||||
service_api_client.get_global_notification_count(service_id)
|
||||
)
|
||||
remaining_global_messages = global_limit - global_messages_count.get(
|
||||
"count"
|
||||
)
|
||||
return {
|
||||
"global_message_limit": global_limit,
|
||||
"daily_global_messages_remaining": remaining_global_messages,
|
||||
}
|
||||
if current_app:
|
||||
global_limit = current_app.config["GLOBAL_SERVICE_MESSAGE_LIMIT"]
|
||||
global_messages_count = service_api_client.get_global_notification_count()
|
||||
remaining_global_messages = global_limit - global_messages_count
|
||||
return {"daily_global_messages_remaining": remaining_global_messages}
|
||||
|
||||
@application.before_request
|
||||
def record_start_time():
|
||||
@@ -342,6 +308,7 @@ def init_app(application):
|
||||
|
||||
application.url_map.converters["uuid"].to_python = lambda self, value: value
|
||||
application.url_map.converters["template_type"] = TemplateTypeConverter
|
||||
application.url_map.converters["ticket_type"] = TicketTypeConverter
|
||||
application.url_map.converters["simple_date"] = SimpleDateTypeConverter
|
||||
|
||||
|
||||
@@ -364,23 +331,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():
|
||||
@@ -388,7 +343,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)
|
||||
|
||||
@@ -410,10 +364,6 @@ def load_service_before_request():
|
||||
request_ctx.service = Service(
|
||||
service_api_client.get_service(service_id)["data"]
|
||||
)
|
||||
stats = service_api_client.get_service_statistics(
|
||||
service_id, limit_days=7
|
||||
)
|
||||
request_ctx.service.stats = stats
|
||||
except HTTPError as exc:
|
||||
# if service id isn't real, then 404 rather than 500ing later because we expect service to be set
|
||||
if exc.status_code == 404:
|
||||
@@ -581,8 +531,7 @@ def add_template_filters(application):
|
||||
format_datetime,
|
||||
format_datetime_24h,
|
||||
format_datetime_normal,
|
||||
format_datetime_scheduled_notification,
|
||||
format_datetime_table,
|
||||
format_datetime_short,
|
||||
valid_phone_number,
|
||||
linkable_name,
|
||||
format_date,
|
||||
@@ -620,7 +569,6 @@ def add_template_filters(application):
|
||||
format_mobile_network,
|
||||
format_yes_no,
|
||||
square_metres_to_square_miles,
|
||||
convert_markdown_template,
|
||||
]:
|
||||
application.add_template_filter(fn)
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
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" />
|
||||
|
||||
<meta property="og:image" content="/static/images/notify-og-image.png">
|
||||
<meta property="og:image" content="/static/images/usa-opengraph-image.png">
|
||||
</head>
|
||||
|
||||
<body class="usa-template__body">
|
||||
@@ -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>
|
||||
@@ -104,4 +107,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 283 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="719px" height="720px" viewBox="0 0 129.996 130.238" enable-background="new 0 0 129.996 130.238"
|
||||
xml:space="preserve">
|
||||
<rect fill="#00538E" width="129.996" height="130.238"/>
|
||||
<path fill="#FFFFFF" d="M78.611,96.076c0,9.138-7.175,12.953-15.451,12.688c-2.938-0.095-5.799-0.838-8.916-1.855l0.707-6.578
|
||||
c2.638,1.086,6.057,2.263,8.92,2.219c3.74-0.058,7.477-1.794,7.477-6.124c0-3.335-2.67-4.976-5.252-6.283
|
||||
c-5.709-2.896-12.275-4.907-12.275-12.998c0-7.65,6.084-11.64,13.625-11.64c2.741,0,6.014,0.477,9.299,1.641l-0.985,6.478
|
||||
c-3.577-1.403-5.14-1.901-8.078-1.901c-3.253,0-6.597,1.143-6.597,4.896c0,2.933,3.01,4.456,5.248,5.573
|
||||
C72.398,85.218,78.611,87.703,78.611,96.076"/>
|
||||
<path fill="#FFFFFF" d="M44.561,106.8c-2.632,0.901-6.662,1.964-13.092,1.964c-13.195,0-22.027-8.529-22.027-21.805
|
||||
c0-12.882,9.375-21.455,22.086-21.455c6.036,0,8.727,0.896,12.541,2.212l-0.574,7.216c-3.51-2.294-6.498-3.211-11.674-3.211
|
||||
c-9.09,0-15.158,6.578-15.115,15.412c0.043,9.506,6.648,15.416,14.822,15.416c2.437,0,4.508-0.178,5.996-0.569V91.15h-7.861v-6.103
|
||||
h14.898V106.8"/>
|
||||
<polyline fill="#FFFFFF" points="102.041,64.587 112.063,86.918 105.08,86.918 102.041,79.497 98.996,86.918 92.014,86.918
|
||||
102.041,64.587 "/>
|
||||
<polyline fill="#FFFFFF" points="112.178,87.179 121.507,108.029 114.176,108.029 107.203,91.839 112.178,87.179 "/>
|
||||
<polyline fill="#FFFFFF" points="91.9,87.183 96.873,91.841 93.199,100.24 102.027,95.611 107.109,99.525 89.727,108.031
|
||||
82.57,108.031 91.9,87.183 "/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="187" height="230" viewBox="0 0 187 230" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M89.1324 230V203.125H57.936C60.0752 226.499 79.7735 230 89.1324 230Z" fill="#2491FF"/>
|
||||
<path d="M89.1324 230V203.125H121.22C119.02 226.499 98.7587 230 89.1324 230Z" fill="#005EA2"/>
|
||||
<path d="M71.4869 19.5185C71.4869 3.35574 82.3456 0 89.1324 0V194.166H0V174.861L22.1703 153.759V87.3122C22.1703 47.8033 53.8415 24.1578 71.4869 19.5185Z" fill="#2491FF"/>
|
||||
<path d="M89.1324 0C95.9192 0 106.778 3.35574 106.778 19.5185C112.193 20.9422 118.928 24.1557 125.664 29.0681C118.405 36.2889 113.911 46.2873 113.911 57.3353C113.911 79.3543 131.761 97.2043 153.78 97.2043C154.557 97.2043 155.293 97.2481 156.058 97.2043L156.094 153.759L178.265 174.861V194.166H89.1324V0Z" fill="#005EA2"/>
|
||||
<path d="M186.435 57.1535C186.435 74.9784 171.985 89.4283 154.16 89.4283C136.335 89.4283 121.885 74.9784 121.885 57.1535C121.885 39.3286 136.335 24.8786 154.16 24.8786C171.985 24.8786 186.435 39.3286 186.435 57.1535Z" fill="#D54309"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg width="1596" height="400" viewBox="0 0 1596 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M155.013 400V353.26H100.758C104.479 393.912 138.736 400 155.013 400Z" fill="#58B4FF"/>
|
||||
<path d="M155.013 400V353.26H210.818C206.991 393.912 171.754 400 155.013 400Z" fill="#0076D6"/>
|
||||
<path d="M124.325 33.9452C124.325 5.83608 143.21 0 155.013 0V337.68H0V304.105L38.5571 267.407V151.847C38.5571 83.1361 93.6374 42.0136 124.325 33.9452Z" fill="#58B4FF"/>
|
||||
<path d="M155.013 0C166.816 0 185.701 5.83608 185.701 33.9452C195.118 36.4212 206.832 42.0098 218.547 50.5533C205.922 63.1111 198.106 80.4997 198.106 99.7136C198.106 138.008 229.15 169.051 267.444 169.051C268.795 169.051 270.074 169.127 271.406 169.051L271.469 267.407L310.026 304.105V337.68H155.013V0Z" fill="#0076D6"/>
|
||||
<path d="M324.234 99.3973C324.234 130.397 299.104 155.528 268.104 155.528C237.104 155.528 211.974 130.397 211.974 99.3973C211.974 68.3975 237.104 43.2672 268.104 43.2672C299.104 43.2672 324.234 68.3975 324.234 99.3973Z" fill="#D54309"/>
|
||||
<path d="M416.684 290.469C414.747 290.469 413.25 289.94 412.194 288.884C411.137 287.651 410.609 286.154 410.609 284.393V111.909C410.609 109.972 411.137 108.475 412.194 107.418C413.25 106.186 414.747 105.569 416.684 105.569H428.835C430.948 105.569 432.532 106.098 433.589 107.154C434.646 108.035 435.262 108.739 435.438 109.267L523.133 243.98V111.909C523.133 109.972 523.662 108.475 524.718 107.418C525.775 106.186 527.271 105.569 529.208 105.569H541.887C543.824 105.569 545.321 106.186 546.378 107.418C547.61 108.475 548.227 109.972 548.227 111.909V284.129C548.227 285.89 547.61 287.387 546.378 288.62C545.321 289.852 543.912 290.469 542.151 290.469H529.473C527.536 290.469 526.039 289.94 524.982 288.884C524.102 287.827 523.485 287.123 523.133 286.771L435.702 153.379V284.393C435.702 286.154 435.086 287.651 433.853 288.884C432.797 289.94 431.3 290.469 429.363 290.469H416.684Z" fill="#EDEFF0"/>
|
||||
<path d="M636.275 293.11C622.892 293.11 611.71 290.557 602.729 285.45C593.748 280.343 586.88 273.299 582.126 264.319C577.371 255.162 574.73 244.772 574.201 233.15C574.025 230.156 573.937 226.37 573.937 221.792C573.937 217.037 574.025 213.251 574.201 210.434C574.73 198.635 577.371 188.246 582.126 179.265C587.056 170.284 594.012 163.24 602.993 158.133C611.974 153.027 623.068 150.473 636.275 150.473C649.482 150.473 660.576 153.027 669.557 158.133C678.537 163.24 685.405 170.284 690.16 179.265C695.09 188.246 697.82 198.635 698.348 210.434C698.524 213.251 698.612 217.037 698.612 221.792C698.612 226.37 698.524 230.156 698.348 233.15C697.82 244.772 695.178 255.162 690.424 264.319C685.669 273.299 678.802 280.343 669.821 285.45C660.84 290.557 649.658 293.11 636.275 293.11ZM636.275 272.771C647.193 272.771 655.909 269.337 662.425 262.47C668.94 255.426 672.462 245.212 672.99 231.829C673.167 229.188 673.255 225.842 673.255 221.792C673.255 217.742 673.167 214.396 672.99 211.754C672.462 198.371 668.94 188.246 662.425 181.378C655.909 174.334 647.193 170.812 636.275 170.812C625.357 170.812 616.552 174.334 609.861 181.378C603.345 188.246 599.911 198.371 599.559 211.754C599.383 214.396 599.295 217.742 599.295 221.792C599.295 225.842 599.383 229.188 599.559 231.829C599.911 245.212 603.345 255.426 609.861 262.47C616.552 269.337 625.357 272.771 636.275 272.771Z" fill="#EDEFF0"/>
|
||||
<path d="M781.377 290.469C771.516 290.469 763.504 288.62 757.341 284.922C751.177 281.048 746.687 275.677 743.869 268.809C741.052 261.765 739.643 253.489 739.643 243.98V174.246H719.04C717.279 174.246 715.782 173.718 714.549 172.661C713.493 171.429 712.965 169.932 712.965 168.171V159.19C712.965 157.429 713.493 156.02 714.549 154.964C715.782 153.731 717.279 153.115 719.04 153.115H739.643V109.003C739.643 107.242 740.171 105.833 741.228 104.777C742.461 103.544 743.957 102.928 745.718 102.928H758.133C759.894 102.928 761.303 103.544 762.359 104.777C763.592 105.833 764.208 107.242 764.208 109.003V153.115H796.962C798.723 153.115 800.132 153.731 801.188 154.964C802.421 156.02 803.037 157.429 803.037 159.19V168.171C803.037 169.932 802.421 171.429 801.188 172.661C800.132 173.718 798.723 174.246 796.962 174.246H764.208V242.131C764.208 250.407 765.617 256.923 768.434 261.677C771.252 266.432 776.271 268.809 783.491 268.809H799.603C801.364 268.809 802.773 269.425 803.83 270.658C805.062 271.715 805.678 273.123 805.678 274.884V284.393C805.678 286.154 805.062 287.651 803.83 288.884C802.773 289.94 801.364 290.469 799.603 290.469H781.377Z" fill="#EDEFF0"/>
|
||||
<path d="M834.371 290.469C832.61 290.469 831.113 289.94 829.88 288.884C828.824 287.651 828.296 286.154 828.296 284.393V159.19C828.296 157.429 828.824 156.02 829.88 154.964C831.113 153.731 832.61 153.115 834.371 153.115H847.05C848.811 153.115 850.219 153.731 851.276 154.964C852.333 156.02 852.861 157.429 852.861 159.19V284.393C852.861 286.154 852.333 287.651 851.276 288.884C850.219 289.94 848.811 290.469 847.05 290.469H834.371ZM832.258 127.493C830.497 127.493 829 126.965 827.767 125.908C826.711 124.675 826.182 123.179 826.182 121.418V107.154C826.182 105.393 826.711 103.984 827.767 102.928C829 101.695 830.497 101.079 832.258 101.079H848.899C850.66 101.079 852.068 101.695 853.125 102.928C854.358 103.984 854.974 105.393 854.974 107.154V121.418C854.974 123.179 854.358 124.675 853.125 125.908C852.068 126.965 850.66 127.493 848.899 127.493H832.258Z" fill="#EDEFF0"/>
|
||||
<path d="M912.078 290.469C910.317 290.469 908.82 289.94 907.588 288.884C906.531 287.651 906.003 286.154 906.003 284.393V174.246H883.551C881.79 174.246 880.293 173.718 879.06 172.661C878.004 171.429 877.476 169.932 877.476 168.171V159.19C877.476 157.429 878.004 156.02 879.06 154.964C880.293 153.731 881.79 153.115 883.551 153.115H906.003V139.908C906.003 130.927 907.5 123.091 910.493 116.399C913.487 109.531 918.242 104.249 924.757 100.551C931.449 96.8525 940.341 95.0036 951.435 95.0036H967.284C969.045 95.0036 970.454 95.6199 971.51 96.8525C972.567 97.9091 973.095 99.3179 973.095 101.079V110.06C973.095 111.821 972.567 113.317 971.51 114.55C970.454 115.607 969.045 116.135 967.284 116.135H951.964C943.687 116.135 938.052 118.336 935.059 122.738C932.065 126.965 930.568 133.128 930.568 141.228V153.115H964.642C966.403 153.115 967.812 153.731 968.869 154.964C969.925 156.02 970.454 157.429 970.454 159.19V168.171C970.454 169.932 969.925 171.429 968.869 172.661C967.812 173.718 966.403 174.246 964.642 174.246H930.568V284.393C930.568 286.154 929.952 287.651 928.719 288.884C927.663 289.94 926.254 290.469 924.493 290.469H912.078Z" fill="#EDEFF0"/>
|
||||
<path d="M1017.94 340.656C1016.53 340.656 1015.3 340.127 1014.24 339.071C1013.18 338.014 1012.65 336.782 1012.65 335.373C1012.65 334.668 1012.74 333.964 1012.92 333.26C1013.09 332.555 1013.45 331.675 1013.98 330.618L1034.31 282.28L983.863 163.152C982.982 161.039 982.542 159.542 982.542 158.662C982.542 157.077 983.071 155.756 984.127 154.7C985.184 153.643 986.504 153.115 988.089 153.115H1001.03C1002.79 153.115 1004.2 153.555 1005.26 154.435C1006.31 155.316 1007.02 156.373 1007.37 157.605L1047.52 253.753L1088.73 157.605C1089.26 156.373 1089.96 155.316 1090.84 154.435C1091.9 153.555 1093.39 153.115 1095.33 153.115H1107.75C1109.33 153.115 1110.65 153.643 1111.71 154.7C1112.76 155.756 1113.29 156.989 1113.29 158.398C1113.29 159.278 1112.85 160.863 1111.97 163.152L1036.96 336.165C1036.43 337.398 1035.63 338.454 1034.58 339.335C1033.7 340.215 1032.29 340.656 1030.35 340.656H1017.94Z" fill="#EDEFF0"/>
|
||||
<path d="M1128.65 290.469C1126.89 290.469 1125.39 289.94 1124.16 288.884C1123.1 287.651 1122.57 286.154 1122.57 284.393V265.375C1122.57 263.614 1123.1 262.205 1124.16 261.149C1125.39 259.916 1126.89 259.3 1128.65 259.3H1147.67C1149.43 259.3 1150.84 259.916 1151.89 261.149C1153.13 262.205 1153.74 263.614 1153.74 265.375V284.393C1153.74 286.154 1153.13 287.651 1151.89 288.884C1150.84 289.94 1149.43 290.469 1147.67 290.469H1128.65Z" fill="#EDEFF0"/>
|
||||
<path d="M1244.66 348.58C1232.68 348.58 1222.82 346.995 1215.07 343.825C1207.5 340.656 1201.51 336.782 1197.11 332.203C1192.89 327.801 1189.8 323.31 1187.87 318.732C1186.11 314.329 1185.14 310.808 1184.96 308.166C1184.79 306.405 1185.31 304.908 1186.55 303.676C1187.96 302.443 1189.45 301.827 1191.04 301.827H1203.72C1205.3 301.827 1206.62 302.179 1207.68 302.883C1208.73 303.588 1209.61 305.084 1210.32 307.374C1211.38 310.015 1212.96 312.921 1215.07 316.09C1217.19 319.436 1220.44 322.254 1224.85 324.543C1229.25 327.008 1235.41 328.241 1243.34 328.241C1251.61 328.241 1258.57 327.096 1264.2 324.807C1269.84 322.694 1274.07 318.82 1276.88 313.185C1279.88 307.726 1281.37 300.154 1281.37 290.469V272.507C1277.15 277.966 1271.6 282.544 1264.73 286.242C1257.86 289.764 1249.24 291.525 1238.85 291.525C1228.99 291.525 1220.53 289.852 1213.49 286.507C1206.45 282.985 1200.63 278.318 1196.06 272.507C1191.65 266.52 1188.31 259.74 1186.02 252.168C1183.91 244.596 1182.67 236.672 1182.32 228.395C1182.14 223.465 1182.14 218.622 1182.32 213.867C1182.67 205.591 1183.91 197.667 1186.02 190.095C1188.31 182.523 1191.65 175.743 1196.06 169.756C1200.63 163.769 1206.45 159.102 1213.49 155.756C1220.53 152.234 1228.99 150.473 1238.85 150.473C1249.41 150.473 1258.13 152.498 1265 156.549C1272.04 160.423 1277.76 165.177 1282.17 170.812V159.454C1282.17 157.693 1282.69 156.196 1283.75 154.964C1284.98 153.731 1286.48 153.115 1288.24 153.115H1300.39C1302.15 153.115 1303.65 153.731 1304.88 154.964C1306.11 156.196 1306.73 157.693 1306.73 159.454V287.563C1306.73 299.361 1304.71 309.839 1300.66 318.996C1296.78 328.153 1290.27 335.373 1281.11 340.656C1272.13 345.938 1259.98 348.58 1244.66 348.58ZM1244.13 270.394C1252.93 270.394 1259.98 268.369 1265.26 264.319C1270.72 260.268 1274.68 255.162 1277.15 248.998C1279.79 242.835 1281.2 236.584 1281.37 230.244C1281.55 227.779 1281.64 224.785 1281.64 221.263C1281.64 217.565 1281.55 214.484 1281.37 212.018C1281.2 205.679 1279.79 199.428 1277.15 193.264C1274.68 187.101 1270.72 181.994 1265.26 177.944C1259.98 173.894 1252.93 171.869 1244.13 171.869C1235.32 171.869 1228.28 173.894 1223 177.944C1217.72 181.818 1213.93 187.013 1211.64 193.529C1209.35 199.868 1208.03 206.736 1207.68 214.132C1207.5 218.71 1207.5 223.377 1207.68 228.131C1208.03 235.527 1209.35 242.483 1211.64 248.998C1213.93 255.338 1217.72 260.533 1223 264.583C1228.28 268.457 1235.32 270.394 1244.13 270.394Z" fill="#EDEFF0"/>
|
||||
<path d="M1394.34 293.11C1380.96 293.11 1369.78 290.557 1360.8 285.45C1351.81 280.343 1344.95 273.299 1340.19 264.319C1335.44 255.162 1332.8 244.772 1332.27 233.15C1332.09 230.156 1332 226.37 1332 221.792C1332 217.037 1332.09 213.251 1332.27 210.434C1332.8 198.635 1335.44 188.246 1340.19 179.265C1345.12 170.284 1352.08 163.24 1361.06 158.133C1370.04 153.027 1381.13 150.473 1394.34 150.473C1407.55 150.473 1418.64 153.027 1427.62 158.133C1436.6 163.24 1443.47 170.284 1448.23 179.265C1453.16 188.246 1455.89 198.635 1456.42 210.434C1456.59 213.251 1456.68 217.037 1456.68 221.792C1456.68 226.37 1456.59 230.156 1456.42 233.15C1455.89 244.772 1453.25 255.162 1448.49 264.319C1443.74 273.299 1436.87 280.343 1427.89 285.45C1418.91 290.557 1407.72 293.11 1394.34 293.11ZM1394.34 272.771C1405.26 272.771 1413.98 269.337 1420.49 262.47C1427.01 255.426 1430.53 245.212 1431.06 231.829C1431.23 229.188 1431.32 225.842 1431.32 221.792C1431.32 217.742 1431.23 214.396 1431.06 211.754C1430.53 198.371 1427.01 188.246 1420.49 181.378C1413.98 174.334 1405.26 170.812 1394.34 170.812C1383.42 170.812 1374.62 174.334 1367.93 181.378C1361.41 188.246 1357.98 198.371 1357.63 211.754C1357.45 214.396 1357.36 217.742 1357.36 221.792C1357.36 225.842 1357.45 229.188 1357.63 231.829C1357.98 245.212 1361.41 255.426 1367.93 262.47C1374.62 269.337 1383.42 272.771 1394.34 272.771Z" fill="#EDEFF0"/>
|
||||
<path d="M1524.74 290.469C1522.1 290.469 1519.98 289.852 1518.4 288.62C1516.99 287.387 1515.85 285.802 1514.97 283.865L1466.1 161.303C1465.75 160.423 1465.57 159.542 1465.57 158.662C1465.57 157.077 1466.1 155.756 1467.16 154.7C1468.21 153.643 1469.53 153.115 1471.12 153.115H1483.8C1485.73 153.115 1487.23 153.643 1488.29 154.7C1489.34 155.756 1489.96 156.725 1490.14 157.605L1530.55 261.677L1570.7 157.605C1571.05 156.725 1571.67 155.756 1572.55 154.7C1573.61 153.643 1575.1 153.115 1577.04 153.115H1589.98C1591.39 153.115 1592.62 153.643 1593.68 154.7C1594.91 155.756 1595.53 157.077 1595.53 158.662C1595.53 159.542 1595.35 160.423 1595 161.303L1545.87 283.865C1545.17 285.802 1544.02 287.387 1542.44 288.62C1541.03 289.852 1538.91 290.469 1536.1 290.469H1524.74Z" fill="#EDEFF0"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg width="1596" height="400" viewBox="0 0 1596 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M155.013 400V353.26H100.759C104.479 393.912 138.737 400 155.013 400Z" fill="#2491FF"/>
|
||||
<path d="M155.013 400V353.26H210.818C206.992 393.912 171.755 400 155.013 400Z" fill="#005EA2"/>
|
||||
<path d="M124.326 33.9452C124.326 5.83608 143.21 0 155.014 0V337.68H0V304.105L38.5573 267.407V151.847C38.5573 83.1361 93.6378 42.0136 124.326 33.9452Z" fill="#2491FF"/>
|
||||
<path d="M155.014 0C166.817 0 185.702 5.83608 185.702 33.9452C195.119 36.4212 206.833 42.0098 218.548 50.5533C205.923 63.1111 198.107 80.4997 198.107 99.7136C198.107 138.008 229.151 169.051 267.445 169.051C268.796 169.051 270.075 169.127 271.407 169.051L271.47 267.407L310.027 304.105V337.68H155.014V0Z" fill="#005EA2"/>
|
||||
<path d="M324.236 99.3973C324.236 130.397 299.105 155.528 268.105 155.528C237.105 155.528 211.975 130.397 211.975 99.3973C211.975 68.3975 237.105 43.2672 268.105 43.2672C299.105 43.2672 324.236 68.3975 324.236 99.3973Z" fill="#D54309"/>
|
||||
<path d="M416.686 290.469C414.749 290.469 413.252 289.94 412.195 288.884C411.139 287.651 410.61 286.154 410.61 284.393V111.909C410.61 109.972 411.139 108.475 412.195 107.418C413.252 106.186 414.749 105.569 416.686 105.569H428.836C430.95 105.569 432.534 106.098 433.591 107.154C434.647 108.035 435.264 108.739 435.44 109.267L523.135 243.98V111.909C523.135 109.972 523.664 108.475 524.72 107.418C525.777 106.186 527.274 105.569 529.211 105.569H541.89C543.827 105.569 545.323 106.186 546.38 107.418C547.613 108.475 548.229 109.972 548.229 111.909V284.129C548.229 285.89 547.613 287.387 546.38 288.62C545.323 289.852 543.915 290.469 542.154 290.469H529.475C527.538 290.469 526.041 289.94 524.984 288.884C524.104 287.827 523.488 287.123 523.135 286.771L435.704 153.379V284.393C435.704 286.154 435.088 287.651 433.855 288.884C432.798 289.94 431.302 290.469 429.365 290.469H416.686Z" fill="#2D2E2F"/>
|
||||
<path d="M636.277 293.11C622.894 293.11 611.712 290.557 602.731 285.45C593.75 280.343 586.883 273.299 582.128 264.319C577.373 255.162 574.732 244.772 574.204 233.15C574.028 230.156 573.94 226.37 573.94 221.792C573.94 217.037 574.028 213.251 574.204 210.434C574.732 198.635 577.373 188.246 582.128 179.265C587.059 170.284 594.014 163.24 602.995 158.133C611.976 153.027 623.07 150.473 636.277 150.473C649.485 150.473 660.579 153.027 669.559 158.133C678.54 163.24 685.408 170.284 690.163 179.265C695.093 188.246 697.823 198.635 698.351 210.434C698.527 213.251 698.615 217.037 698.615 221.792C698.615 226.37 698.527 230.156 698.351 233.15C697.823 244.772 695.181 255.162 690.427 264.319C685.672 273.299 678.804 280.343 669.824 285.45C660.843 290.557 649.661 293.11 636.277 293.11ZM636.277 272.771C647.195 272.771 655.912 269.337 662.428 262.47C668.943 255.426 672.465 245.212 672.993 231.829C673.169 229.188 673.257 225.842 673.257 221.792C673.257 217.742 673.169 214.396 672.993 211.754C672.465 198.371 668.943 188.246 662.428 181.378C655.912 174.334 647.195 170.812 636.277 170.812C625.359 170.812 616.555 174.334 609.863 181.378C603.348 188.246 599.914 198.371 599.562 211.754C599.385 214.396 599.297 217.742 599.297 221.792C599.297 225.842 599.385 229.188 599.562 231.829C599.914 245.212 603.348 255.426 609.863 262.47C616.555 269.337 625.359 272.771 636.277 272.771Z" fill="#2D2E2F"/>
|
||||
<path d="M781.381 290.469C771.519 290.469 763.507 288.62 757.344 284.922C751.18 281.048 746.69 275.677 743.872 268.809C741.055 261.765 739.646 253.489 739.646 243.98V174.246H719.043C717.282 174.246 715.785 173.718 714.552 172.661C713.496 171.429 712.968 169.932 712.968 168.171V159.19C712.968 157.429 713.496 156.02 714.552 154.964C715.785 153.731 717.282 153.115 719.043 153.115H739.646V109.003C739.646 107.242 740.174 105.833 741.231 104.777C742.464 103.544 743.96 102.928 745.721 102.928H758.136C759.897 102.928 761.306 103.544 762.362 104.777C763.595 105.833 764.211 107.242 764.211 109.003V153.115H796.965C798.726 153.115 800.135 153.731 801.191 154.964C802.424 156.02 803.04 157.429 803.04 159.19V168.171C803.04 169.932 802.424 171.429 801.191 172.661C800.135 173.718 798.726 174.246 796.965 174.246H764.211V242.131C764.211 250.407 765.62 256.923 768.438 261.677C771.255 266.432 776.274 268.809 783.494 268.809H799.607C801.368 268.809 802.776 269.425 803.833 270.658C805.066 271.715 805.682 273.123 805.682 274.884V284.393C805.682 286.154 805.066 287.651 803.833 288.884C802.776 289.94 801.368 290.469 799.607 290.469H781.381Z" fill="#2D2E2F"/>
|
||||
<path d="M834.374 290.469C832.613 290.469 831.117 289.94 829.884 288.884C828.827 287.651 828.299 286.154 828.299 284.393V159.19C828.299 157.429 828.827 156.02 829.884 154.964C831.117 153.731 832.613 153.115 834.374 153.115H847.053C848.814 153.115 850.223 153.731 851.28 154.964C852.336 156.02 852.864 157.429 852.864 159.19V284.393C852.864 286.154 852.336 287.651 851.28 288.884C850.223 289.94 848.814 290.469 847.053 290.469H834.374ZM832.261 127.493C830.5 127.493 829.004 126.965 827.771 125.908C826.714 124.675 826.186 123.179 826.186 121.418V107.154C826.186 105.393 826.714 103.984 827.771 102.928C829.004 101.695 830.5 101.079 832.261 101.079H848.902C850.663 101.079 852.072 101.695 853.129 102.928C854.361 103.984 854.978 105.393 854.978 107.154V121.418C854.978 123.179 854.361 124.675 853.129 125.908C852.072 126.965 850.663 127.493 848.902 127.493H832.261Z" fill="#2D2E2F"/>
|
||||
<path d="M912.082 290.469C910.321 290.469 908.824 289.94 907.592 288.884C906.535 287.651 906.007 286.154 906.007 284.393V174.246H883.555C881.794 174.246 880.297 173.718 879.064 172.661C878.008 171.429 877.479 169.932 877.479 168.171V159.19C877.479 157.429 878.008 156.02 879.064 154.964C880.297 153.731 881.794 153.115 883.555 153.115H906.007V139.908C906.007 130.927 907.504 123.091 910.497 116.399C913.491 109.531 918.245 104.249 924.761 100.551C931.453 96.8525 940.345 95.0035 951.439 95.0035H967.288C969.049 95.0035 970.458 95.6199 971.514 96.8525C972.571 97.9091 973.099 99.3179 973.099 101.079V110.06C973.099 111.821 972.571 113.317 971.514 114.55C970.458 115.607 969.049 116.135 967.288 116.135H951.968C943.691 116.135 938.056 118.336 935.062 122.738C932.069 126.965 930.572 133.128 930.572 141.228V153.115H964.646C966.407 153.115 967.816 153.731 968.873 154.964C969.929 156.02 970.458 157.429 970.458 159.19V168.171C970.458 169.932 969.929 171.429 968.873 172.661C967.816 173.718 966.407 174.246 964.646 174.246H930.572V284.393C930.572 286.154 929.956 287.651 928.723 288.884C927.667 289.94 926.258 290.469 924.497 290.469H912.082Z" fill="#2D2E2F"/>
|
||||
<path d="M1017.94 340.656C1016.53 340.656 1015.3 340.127 1014.24 339.071C1013.19 338.014 1012.66 336.782 1012.66 335.373C1012.66 334.668 1012.75 333.964 1012.92 333.26C1013.1 332.555 1013.45 331.675 1013.98 330.618L1034.32 282.28L983.867 163.152C982.987 161.039 982.546 159.542 982.546 158.662C982.546 157.077 983.075 155.756 984.131 154.7C985.188 153.643 986.509 153.115 988.093 153.115H1001.04C1002.8 153.115 1004.21 153.555 1005.26 154.435C1006.32 155.316 1007.02 156.373 1007.38 157.605L1047.53 253.753L1088.73 157.605C1089.26 156.373 1089.96 155.316 1090.84 154.435C1091.9 153.555 1093.4 153.115 1095.34 153.115H1107.75C1109.33 153.115 1110.66 153.643 1111.71 154.7C1112.77 155.756 1113.3 156.989 1113.3 158.398C1113.3 159.278 1112.86 160.863 1111.98 163.152L1036.96 336.165C1036.43 337.398 1035.64 338.454 1034.58 339.335C1033.7 340.215 1032.29 340.656 1030.36 340.656H1017.94Z" fill="#2D2E2F"/>
|
||||
<path d="M1128.65 290.469C1126.89 290.469 1125.4 289.94 1124.16 288.884C1123.11 287.651 1122.58 286.154 1122.58 284.393V265.375C1122.58 263.614 1123.11 262.205 1124.16 261.149C1125.4 259.916 1126.89 259.3 1128.65 259.3H1147.67C1149.43 259.3 1150.84 259.916 1151.9 261.149C1153.13 262.205 1153.75 263.614 1153.75 265.375V284.393C1153.75 286.154 1153.13 287.651 1151.9 288.884C1150.84 289.94 1149.43 290.469 1147.67 290.469H1128.65Z" fill="#2D2E2F"/>
|
||||
<path d="M1244.66 348.58C1232.69 348.58 1222.83 346.995 1215.08 343.825C1207.51 340.656 1201.52 336.782 1197.12 332.203C1192.89 327.801 1189.81 323.31 1187.87 318.732C1186.11 314.329 1185.14 310.808 1184.97 308.166C1184.79 306.405 1185.32 304.908 1186.55 303.676C1187.96 302.443 1189.46 301.827 1191.04 301.827H1203.72C1205.31 301.827 1206.63 302.179 1207.68 302.883C1208.74 303.588 1209.62 305.084 1210.32 307.374C1211.38 310.015 1212.97 312.921 1215.08 316.09C1217.19 319.436 1220.45 322.254 1224.85 324.543C1229.25 327.008 1235.42 328.241 1243.34 328.241C1251.62 328.241 1258.57 327.096 1264.21 324.807C1269.84 322.694 1274.07 318.82 1276.89 313.185C1279.88 307.726 1281.38 300.154 1281.38 290.469V272.507C1277.15 277.966 1271.61 282.544 1264.74 286.242C1257.87 289.764 1249.24 291.525 1238.85 291.525C1228.99 291.525 1220.54 289.852 1213.49 286.507C1206.45 282.985 1200.64 278.318 1196.06 272.507C1191.66 266.52 1188.31 259.74 1186.02 252.168C1183.91 244.596 1182.68 236.672 1182.33 228.395C1182.15 223.465 1182.15 218.622 1182.33 213.867C1182.68 205.591 1183.91 197.667 1186.02 190.095C1188.31 182.523 1191.66 175.743 1196.06 169.756C1200.64 163.769 1206.45 159.102 1213.49 155.756C1220.54 152.234 1228.99 150.473 1238.85 150.473C1249.42 150.473 1258.13 152.498 1265 156.549C1272.05 160.423 1277.77 165.177 1282.17 170.812V159.454C1282.17 157.693 1282.7 156.196 1283.76 154.964C1284.99 153.731 1286.49 153.115 1288.25 153.115H1300.4C1302.16 153.115 1303.65 153.731 1304.89 154.964C1306.12 156.196 1306.74 157.693 1306.74 159.454V287.563C1306.74 299.361 1304.71 309.839 1300.66 318.996C1296.79 328.153 1290.27 335.373 1281.11 340.656C1272.13 345.938 1259.98 348.58 1244.66 348.58ZM1244.13 270.394C1252.94 270.394 1259.98 268.369 1265.27 264.319C1270.73 260.268 1274.69 255.162 1277.15 248.998C1279.79 242.835 1281.2 236.584 1281.38 230.244C1281.55 227.779 1281.64 224.785 1281.64 221.263C1281.64 217.565 1281.55 214.484 1281.38 212.018C1281.2 205.679 1279.79 199.428 1277.15 193.264C1274.69 187.101 1270.73 181.994 1265.27 177.944C1259.98 173.894 1252.94 171.869 1244.13 171.869C1235.33 171.869 1228.29 173.894 1223 177.944C1217.72 181.818 1213.93 187.013 1211.65 193.529C1209.36 199.868 1208.04 206.736 1207.68 214.132C1207.51 218.71 1207.51 223.377 1207.68 228.131C1208.04 235.527 1209.36 242.483 1211.65 248.998C1213.93 255.338 1217.72 260.533 1223 264.583C1228.29 268.457 1235.33 270.394 1244.13 270.394Z" fill="#2D2E2F"/>
|
||||
<path d="M1394.35 293.11C1380.96 293.11 1369.78 290.557 1360.8 285.45C1351.82 280.343 1344.95 273.299 1340.2 264.319C1335.44 255.162 1332.8 244.772 1332.27 233.15C1332.1 230.156 1332.01 226.37 1332.01 221.792C1332.01 217.037 1332.1 213.251 1332.27 210.434C1332.8 198.635 1335.44 188.246 1340.2 179.265C1345.13 170.284 1352.08 163.24 1361.07 158.133C1370.05 153.027 1381.14 150.473 1394.35 150.473C1407.55 150.473 1418.65 153.027 1427.63 158.133C1436.61 163.24 1443.48 170.284 1448.23 179.265C1453.16 188.246 1455.89 198.635 1456.42 210.434C1456.6 213.251 1456.69 217.037 1456.69 221.792C1456.69 226.37 1456.6 230.156 1456.42 233.15C1455.89 244.772 1453.25 255.162 1448.5 264.319C1443.74 273.299 1436.87 280.343 1427.89 285.45C1418.91 290.557 1407.73 293.11 1394.35 293.11ZM1394.35 272.771C1405.27 272.771 1413.98 269.337 1420.5 262.47C1427.01 255.426 1430.53 245.212 1431.06 231.829C1431.24 229.188 1431.33 225.842 1431.33 221.792C1431.33 217.742 1431.24 214.396 1431.06 211.754C1430.53 198.371 1427.01 188.246 1420.5 181.378C1413.98 174.334 1405.27 170.812 1394.35 170.812C1383.43 170.812 1374.62 174.334 1367.93 181.378C1361.42 188.246 1357.98 198.371 1357.63 211.754C1357.46 214.396 1357.37 217.742 1357.37 221.792C1357.37 225.842 1357.46 229.188 1357.63 231.829C1357.98 245.212 1361.42 255.426 1367.93 262.47C1374.62 269.337 1383.43 272.771 1394.35 272.771Z" fill="#2D2E2F"/>
|
||||
<path d="M1524.75 290.469C1522.1 290.469 1519.99 289.852 1518.41 288.62C1517 287.387 1515.85 285.802 1514.97 283.865L1466.11 161.303C1465.75 160.423 1465.58 159.542 1465.58 158.662C1465.58 157.077 1466.11 155.756 1467.16 154.7C1468.22 153.643 1469.54 153.115 1471.12 153.115H1483.8C1485.74 153.115 1487.24 153.643 1488.29 154.7C1489.35 155.756 1489.97 156.725 1490.14 157.605L1530.56 261.677L1570.71 157.605C1571.06 156.725 1571.67 155.756 1572.56 154.7C1573.61 153.643 1575.11 153.115 1577.05 153.115H1589.99C1591.4 153.115 1592.63 153.643 1593.69 154.7C1594.92 155.756 1595.54 157.077 1595.54 158.662C1595.54 159.542 1595.36 160.423 1595.01 161.303L1545.88 283.865C1545.17 285.802 1544.03 287.387 1542.44 288.62C1541.03 289.852 1538.92 290.469 1536.1 290.469H1524.75Z" fill="#2D2E2F"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 52 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="260.000000pt" height="260.000000pt" viewBox="0 0 260.000000 260.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,260.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1177 2586 c-67 -29 -117 -107 -117 -183 0 -12 -15 -24 -48 -37 -175
|
||||
-69 -342 -229 -426 -406 -74 -156 -77 -183 -78 -657 l-1 -423 -123 -116 -124
|
||||
-117 0 -109 0 -108 996 2 997 3 -1 105 -1 105 -123 117 -123 118 -1 310 c0
|
||||
170 -3 313 -7 316 -4 4 -26 8 -50 9 -105 6 -216 59 -292 140 -152 161 -161
|
||||
417 -22 583 35 42 31 48 -71 97 -42 20 -84 41 -92 45 -8 4 -18 28 -22 54 -14
|
||||
100 -80 158 -182 162 -34 1 -74 -4 -89 -10z"/>
|
||||
<path d="M1854 2297 c-64 -28 -95 -48 -134 -89 -65 -68 -99 -163 -94 -265 11
|
||||
-271 317 -431 547 -286 163 103 215 311 122 482 -35 64 -91 114 -165 149 -77
|
||||
37 -202 41 -276 9z"/>
|
||||
<path d="M910 282 c0 -32 32 -116 59 -154 114 -161 427 -170 566 -16 42 47 75
|
||||
119 75 165 l0 23 -350 0 c-332 0 -350 -1 -350 -18z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,19 +1 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
|
Before Width: | Height: | Size: 101 KiB |
@@ -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 |
@@ -1,313 +0,0 @@
|
||||
(function (window) {
|
||||
|
||||
if (document.getElementById('activityChartContainer')) {
|
||||
|
||||
const COLORS = {
|
||||
delivered: '#0076d6',
|
||||
failed: '#fa9441',
|
||||
text: '#666'
|
||||
};
|
||||
|
||||
const FONT_SIZE = 16;
|
||||
const FONT_WEIGHT = 'bold';
|
||||
const MAX_Y = 120;
|
||||
|
||||
const createChart = function(containerId, labels, deliveredData, failedData) {
|
||||
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);
|
||||
|
||||
// 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 }
|
||||
];
|
||||
|
||||
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)));
|
||||
const y = d3.scaleSqrt()
|
||||
.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, 10)) // Generate up to 10 ticks based on the data
|
||||
.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 // Ensure there's a value for failed, even if it's 0
|
||||
}));
|
||||
|
||||
// Stack the data
|
||||
const stack = d3.stack()
|
||||
.keys(['delivered', 'failed'])
|
||||
.order(d3.stackOrderNone)
|
||||
.offset(d3.stackOffsetNone);
|
||||
|
||||
const series = stack(stackData);
|
||||
|
||||
// Color scale
|
||||
const color = d3.scaleOrdinal()
|
||||
.domain(['delivered', 'failed'])
|
||||
.range([COLORS.delivered, COLORS.failed]);
|
||||
|
||||
// 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));
|
||||
|
||||
barGroups.selectAll('rect')
|
||||
.data(d => d)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.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 => y(d[0]) - y(d[1]));
|
||||
};
|
||||
|
||||
// Function to create an accessible table
|
||||
const createTable = function(tableId, chartType, labels, deliveredData, failedData) {
|
||||
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'];
|
||||
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);
|
||||
|
||||
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 url = type === 'service' ? `/daily_stats.json` : `/daily_stats_by_user.json`;
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
labels = [];
|
||||
deliveredData = [];
|
||||
failedData = [];
|
||||
|
||||
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);
|
||||
|
||||
// Calculate the total number of messages
|
||||
totalMessages += data[dateString].sms.delivered + data[dateString].sms.failure;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(error => console.error('Error fetching daily stats:', error));
|
||||
};
|
||||
|
||||
const handleDropdownChange = function(event) {
|
||||
const selectedValue = event.target.value;
|
||||
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);
|
||||
|
||||
// Update ARIA live region
|
||||
const liveRegion = document.getElementById('aria-live-account');
|
||||
liveRegion.textContent = `Data updated for ${selectedText} - last 7 days`;
|
||||
|
||||
// Switch tables based on dropdown selection
|
||||
const selectedTable = selectedValue === "individual" ? "table1" : "table2";
|
||||
const tables = document.querySelectorAll('.table-overflow-x-auto');
|
||||
tables.forEach(function(table) {
|
||||
table.classList.add('hidden'); // Hide all tables by adding the hidden class
|
||||
table.classList.remove('visible'); // Ensure they are not visible
|
||||
});
|
||||
const tableToShow = document.getElementById(selectedTable);
|
||||
tableToShow.classList.remove('hidden'); // Remove hidden class
|
||||
tableToShow.classList.add('visible'); // Add visible class
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize activityChart chart and table with service data by default
|
||||
fetchData('service');
|
||||
|
||||
// Add event listener to the dropdown
|
||||
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) {
|
||||
createChart('#weeklyChart', labels, deliveredData, failedData);
|
||||
createTable('weeklyTable', 'activityChart', labels, deliveredData, failedData);
|
||||
}
|
||||
});
|
||||
|
||||
// Export functions for testing
|
||||
window.createChart = createChart;
|
||||
window.createTable = createTable;
|
||||
window.handleDropdownChange = handleDropdownChange;
|
||||
window.fetchData = fetchData;
|
||||
}
|
||||
|
||||
})(window);
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
return $(`<div class="selection-footer${stickyClass}">
|
||||
<button
|
||||
class="govuk-button govuk-button--secondary selection-footer__button usa-button usa-button--outline"
|
||||
class="govuk-button govuk-button--secondary selection-footer__button"
|
||||
aria-expanded="${expanded ? 'true' : 'false'}"
|
||||
aria-controls="${this.fieldsetId}">
|
||||
${buttonContent}
|
||||
|
||||
15
app/assets/javascripts/consent.js
Normal file
@@ -0,0 +1,15 @@
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
function hasConsentFor (cookieCategory, consentCookie) {
|
||||
if (consentCookie === undefined) { consentCookie = window.GOVUK.getConsentCookie(); }
|
||||
|
||||
if (consentCookie === null) { return false; }
|
||||
|
||||
if (!(cookieCategory in consentCookie)) { return false; }
|
||||
|
||||
return consentCookie[cookieCategory];
|
||||
}
|
||||
|
||||
window.GOVUK.hasConsentFor = hasConsentFor;
|
||||
})(window);
|
||||
104
app/assets/javascripts/cookieMessage.js
Normal file
@@ -0,0 +1,104 @@
|
||||
window.GOVUK = window.GOVUK || {};
|
||||
window.GOVUK.Modules = window.GOVUK.Modules || {};
|
||||
|
||||
(function (Modules) {
|
||||
function CookieBanner () { }
|
||||
|
||||
CookieBanner.clearOldCookies = function (consent) {
|
||||
var gaCookies = ['_ga', '_gid'];
|
||||
|
||||
// clear old cookie set by our previous JS, set on the www domain
|
||||
if (window.GOVUK.cookie('seen_cookie_message')) {
|
||||
document.cookie = 'seen_cookie_message=;expires=' + new Date().toGMTString() + ';path=/';
|
||||
}
|
||||
|
||||
if (consent === null) {
|
||||
for (var i = 0; i < gaCookies.length; i++) {
|
||||
if (window.GOVUK.cookie(gaCookies[i])) {
|
||||
// GA cookies are set on the base domain so need the www stripping
|
||||
var cookieString = gaCookies[i] + '=;expires=' + new Date().toGMTString() + ';domain=' + window.location.hostname.replace(/^www\./, '.') + ';path=/';
|
||||
document.cookie = cookieString;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.start = function ($module) {
|
||||
this.$module = $module[0];
|
||||
this.$module.hideCookieMessage = this.hideCookieMessage.bind(this);
|
||||
this.$module.showConfirmationMessage = this.showConfirmationMessage.bind(this);
|
||||
this.$module.setCookieConsent = this.setCookieConsent.bind(this);
|
||||
|
||||
this.$module.cookieBanner = document.querySelector('.notify-cookie-banner');
|
||||
this.$module.cookieBannerConfirmationMessage = this.$module.querySelector('.notify-cookie-banner__confirmation');
|
||||
|
||||
this.setupCookieMessage();
|
||||
};
|
||||
|
||||
CookieBanner.prototype.setupCookieMessage = function () {
|
||||
this.$hideLink = this.$module.querySelector('button[data-hide-cookie-banner]');
|
||||
if (this.$hideLink) {
|
||||
this.$hideLink.addEventListener('click', this.$module.hideCookieMessage);
|
||||
}
|
||||
|
||||
this.$acceptCookiesLink = this.$module.querySelector('button[data-accept-cookies=true]');
|
||||
if (this.$acceptCookiesLink) {
|
||||
this.$acceptCookiesLink.addEventListener('click', () => this.$module.setCookieConsent(true));
|
||||
}
|
||||
|
||||
this.$rejectCookiesLink = this.$module.querySelector('button[data-accept-cookies=false]');
|
||||
if (this.$rejectCookiesLink) {
|
||||
this.$rejectCookiesLink.addEventListener('click', () => this.$module.setCookieConsent(false));
|
||||
}
|
||||
|
||||
this.showCookieMessage();
|
||||
};
|
||||
|
||||
CookieBanner.prototype.showCookieMessage = function () {
|
||||
// Show the cookie banner if not in the cookie settings page
|
||||
if (!this.isInCookiesPage()) {
|
||||
var hasCookiesPolicy = window.GOVUK.cookie('cookies_policy');
|
||||
|
||||
if (this.$module && !hasCookiesPolicy) {
|
||||
this.$module.style.display = 'block';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.hideCookieMessage = function (event) {
|
||||
if (this.$module) {
|
||||
this.$module.style.display = 'none';
|
||||
}
|
||||
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
CookieBanner.prototype.setCookieConsent = function (analyticsConsent) {
|
||||
window.GOVUK.setConsentCookie({ 'analytics': analyticsConsent });
|
||||
|
||||
this.$module.showConfirmationMessage(analyticsConsent);
|
||||
this.$module.cookieBannerConfirmationMessage.focus();
|
||||
|
||||
if (analyticsConsent) { window.GOVUK.initAnalytics(); }
|
||||
};
|
||||
|
||||
CookieBanner.prototype.showConfirmationMessage = function (analyticsConsent) {
|
||||
var messagePrefix = analyticsConsent ? 'You’ve accepted analytics cookies.' : 'You told us not to use analytics cookies.';
|
||||
|
||||
this.$cookieBannerMainContent = document.querySelector('.notify-cookie-banner__wrapper');
|
||||
this.$cookieBannerConfirmationMessage = document.querySelector('.notify-cookie-banner__confirmation-message');
|
||||
|
||||
this.$cookieBannerConfirmationMessage.insertAdjacentText('afterbegin', messagePrefix);
|
||||
this.$cookieBannerMainContent.style.display = 'none';
|
||||
this.$module.cookieBannerConfirmationMessage.style.display = 'block';
|
||||
};
|
||||
|
||||
CookieBanner.prototype.isInCookiesPage = function () {
|
||||
return window.location.pathname === '/cookies';
|
||||
};
|
||||
|
||||
Modules.CookieBanner = CookieBanner;
|
||||
})(window.GOVUK.Modules);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
// Show the current year
|
||||
const currentYearElement = document.getElementById("current-year");
|
||||
|
||||
if (currentYearElement) {
|
||||
currentYearElement.textContent = new Date().getFullYear();
|
||||
}
|
||||
})(window);
|
||||
163
app/assets/javascripts/govuk/cookie-functions.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// used by the cookie banner component
|
||||
|
||||
(function (root) {
|
||||
'use strict';
|
||||
window.GOVUK = window.GOVUK || {};
|
||||
|
||||
var DEFAULT_COOKIE_CONSENT = {
|
||||
'analytics': false
|
||||
};
|
||||
|
||||
var COOKIE_CATEGORIES = {
|
||||
'_ga': 'analytics',
|
||||
'_gid': 'analytics'
|
||||
};
|
||||
|
||||
/*
|
||||
Cookie methods
|
||||
==============
|
||||
|
||||
Usage:
|
||||
|
||||
Setting a cookie:
|
||||
GOVUK.cookie('hobnob', 'tasty', { days: 30 });
|
||||
|
||||
Reading a cookie:
|
||||
GOVUK.cookie('hobnob');
|
||||
|
||||
Deleting a cookie:
|
||||
GOVUK.cookie('hobnob', null);
|
||||
*/
|
||||
window.GOVUK.cookie = function (name, value, options) {
|
||||
if (typeof value !== 'undefined') {
|
||||
if (value === false || value === null) {
|
||||
return window.GOVUK.setCookie(name, '', { days: -1 });
|
||||
} else {
|
||||
// Default expiry date of 30 days
|
||||
if (typeof options === 'undefined') {
|
||||
options = { days: 30 };
|
||||
}
|
||||
return window.GOVUK.setCookie(name, value, options);
|
||||
}
|
||||
} else {
|
||||
return window.GOVUK.getCookie(name);
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.getConsentCookie = function () {
|
||||
var consentCookie = window.GOVUK.cookie('cookies_policy');
|
||||
var consentCookieObj;
|
||||
|
||||
if (consentCookie) {
|
||||
try {
|
||||
consentCookieObj = JSON.parse(consentCookie);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof consentCookieObj !== 'object' && consentCookieObj !== null) {
|
||||
consentCookieObj = JSON.parse(consentCookieObj);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return consentCookieObj;
|
||||
};
|
||||
|
||||
window.GOVUK.setConsentCookie = function (options) {
|
||||
var cookieConsent = window.GOVUK.getConsentCookie();
|
||||
|
||||
if (!cookieConsent) {
|
||||
cookieConsent = JSON.parse(JSON.stringify(DEFAULT_COOKIE_CONSENT));
|
||||
}
|
||||
|
||||
for (var cookieType in options) {
|
||||
cookieConsent[cookieType] = options[cookieType];
|
||||
|
||||
// Delete cookies of that type if consent being set to false
|
||||
if (!options[cookieType]) {
|
||||
for (var cookie in COOKIE_CATEGORIES) {
|
||||
if (COOKIE_CATEGORIES[cookie] === cookieType) {
|
||||
window.GOVUK.cookie(cookie, null);
|
||||
|
||||
if (window.GOVUK.cookie(cookie)) {
|
||||
document.cookie = cookie + '=;expires=' + new Date() + ';domain=' + window.location.hostname.replace(/^www\./, '.') + ';path=/';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.GOVUK.setCookie('cookies_policy', JSON.stringify(cookieConsent), { days: 365 });
|
||||
};
|
||||
|
||||
window.GOVUK.checkConsentCookieCategory = function (cookieName, cookieCategory) {
|
||||
var currentConsentCookie = window.GOVUK.getConsentCookie();
|
||||
|
||||
// If the consent cookie doesn't exist, but the cookie is in our known list, return true
|
||||
if (!currentConsentCookie && COOKIE_CATEGORIES[cookieName]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentConsentCookie = window.GOVUK.getConsentCookie();
|
||||
|
||||
// Sometimes currentConsentCookie is malformed in some of the tests, so we need to handle these
|
||||
try {
|
||||
return currentConsentCookie[cookieCategory];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.checkConsentCookie = function (cookieName, cookieValue) {
|
||||
// If we're setting the consent cookie OR deleting a cookie, allow by default
|
||||
if (cookieName === 'cookies_policy' || (cookieValue === null || cookieValue === false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (COOKIE_CATEGORIES[cookieName]) {
|
||||
var cookieCategory = COOKIE_CATEGORIES[cookieName];
|
||||
|
||||
return window.GOVUK.checkConsentCookieCategory(cookieName, cookieCategory);
|
||||
} else {
|
||||
// Deny the cookie if it is not known to us
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.setCookie = function (name, value, options) {
|
||||
if (window.GOVUK.checkConsentCookie(name, value)) {
|
||||
if (typeof options === 'undefined') {
|
||||
options = {};
|
||||
}
|
||||
var cookieString = name + '=' + value + '; path=/';
|
||||
if (options.days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (options.days * 24 * 60 * 60 * 1000));
|
||||
cookieString = cookieString + '; expires=' + date.toGMTString();
|
||||
}
|
||||
if (document.location.protocol === 'https:') {
|
||||
cookieString = cookieString + '; Secure';
|
||||
}
|
||||
document.cookie = cookieString;
|
||||
}
|
||||
};
|
||||
|
||||
window.GOVUK.getCookie = function (name) {
|
||||
var nameEQ = name + '=';
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0, len = cookies.length; i < len; i++) {
|
||||
var cookie = cookies[i];
|
||||
while (cookie.charAt(0) === ' ') {
|
||||
cookie = cookie.substring(1, cookie.length);
|
||||
}
|
||||
if (cookie.indexOf(nameEQ) === 0) {
|
||||
return decodeURIComponent(cookie.substring(nameEQ.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}(window));
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
(function (window) {
|
||||
|
||||
// Set the target date (10 days before March 15th, 2024)
|
||||
const targetDate = new Date("April 16, 2024 00:00:00").getTime();
|
||||
|
||||
// Set a const for the container to write a conditional statement and only run if it exists
|
||||
const countdownContainer = document.getElementById("countdown-container");
|
||||
|
||||
// Function to update the countdown display
|
||||
function updateCountdown() {
|
||||
|
||||
if (countdownContainer) {
|
||||
const now = new Date().getTime();
|
||||
const difference = targetDate - now;
|
||||
|
||||
// Time calculations for days only
|
||||
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Visibility logic
|
||||
if (days < 0 || days > 10) {
|
||||
// Hide if more than 10 days away OR if already past the date
|
||||
countdownContainer.style.display = "none";
|
||||
} else {
|
||||
// Show if 10 days or less remaining
|
||||
countdownContainer.style.display = "block";
|
||||
document.getElementById("countdown").innerHTML = days + " days ";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expose the updateCountdown function to the outside world
|
||||
window.updateCountdown = updateCountdown;
|
||||
|
||||
// Initial display update
|
||||
updateCountdown();
|
||||
|
||||
// Update the countdown every second (inside the IIFE)
|
||||
setInterval(updateCountdown, 1000);
|
||||
|
||||
})(window);
|
||||
@@ -1,5 +1,12 @@
|
||||
window.GOVUK.Frontend.initAll();
|
||||
|
||||
var consentData = window.GOVUK.getConsentCookie();
|
||||
window.GOVUK.Modules.CookieBanner.clearOldCookies(consentData);
|
||||
|
||||
if (window.GOVUK.hasConsentFor('analytics', consentData)) {
|
||||
window.GOVUK.initAnalytics();
|
||||
}
|
||||
|
||||
$(() => $("time.timeago").timeago());
|
||||
|
||||
var showHideContent = new GOVUK.ShowHideContent();
|
||||
|
||||
36
app/assets/javascripts/previewPane.js
Normal file
@@ -0,0 +1,36 @@
|
||||
(function (global) {
|
||||
|
||||
'use strict';
|
||||
|
||||
$ = global.jQuery;
|
||||
|
||||
let branding_style = $('.usa-radio input[name="branding_style"]:checked');
|
||||
|
||||
if (!branding_style.length) { return; }
|
||||
|
||||
branding_style = branding_style.val();
|
||||
|
||||
const $paneWrapper = $('<div class="govuk-grid-column-full"></div>');
|
||||
const $form = $('form');
|
||||
const previewType = $form.data('previewType');
|
||||
const $previewPane = $(`<iframe src="/_${previewType}?${buildQueryString(['branding_style', branding_style])}" class="branding-preview" scrolling="no"></iframe>`);
|
||||
|
||||
function buildQueryString () {
|
||||
return $.map(arguments, (val, idx) => encodeURI(val[0]) + '=' + encodeURI(val[1])).join('&');
|
||||
}
|
||||
|
||||
function setPreviewPane (e) {
|
||||
const $target = $(e.target);
|
||||
if ($target.attr('name') == 'branding_style') {
|
||||
branding_style = $target.val();
|
||||
}
|
||||
$previewPane.attr('src', `/_${previewType}?${buildQueryString(['branding_style', branding_style])}`);
|
||||
}
|
||||
|
||||
$paneWrapper.append($previewPane);
|
||||
$form.find('.govuk-grid-row').eq(0).prepend($paneWrapper);
|
||||
$form.attr('action', location.pathname.replace(new RegExp(`set-${previewType}-branding$`), `preview-${previewType}-branding`));
|
||||
$form.find('button[type="submit"]').text('Save');
|
||||
|
||||
$('fieldset').on('change', 'input[name="branding_style"]', setPreviewPane);
|
||||
})(window);
|
||||
@@ -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 sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
|
||||
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
|
||||
var totalMessages = sms_sent + sms_remaining_messages;
|
||||
|
||||
// Update the message below the chart
|
||||
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
|
||||
|
||||
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
|
||||
var minSentPercentage = (sms_sent === 0) ? 0 : 0.02;
|
||||
var minSentValue = totalMessages * minSentPercentage;
|
||||
var displaySent = Math.max(sms_sent, 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: ${sms_sent.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: ${sms_remaining_messages.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 = sms_sent.toLocaleString(); // Value for Messages Sent
|
||||
var tdRemaining = document.createElement('td');
|
||||
tdRemaining.textContent = sms_remaining_messages.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);
|
||||
@@ -1,4 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
document.cookie = `timezone=${timeZone}; path=/`;
|
||||
})
|
||||
@@ -1,159 +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);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-tooltip {
|
||||
line-height: 1;
|
||||
.usa-tooltip__body {
|
||||
width: units(mobile);
|
||||
font-size: size("body", 2);
|
||||
height: auto;
|
||||
white-space: wrap;
|
||||
line-height: units(1);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
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;
|
||||
}
|
||||
@@ -48,13 +48,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sms-message-sender, .sms-message-file-name, .sms-message-scheduler, .sms-message-template, .sms-message-sender {
|
||||
margin:0.25rem 0 0;
|
||||
.sms-message-sender {
|
||||
margin: units(1) 0 0;
|
||||
}
|
||||
|
||||
.sms-message-recipient {
|
||||
color: color('gray-cool-90');
|
||||
margin: units(1) 0 units(1);
|
||||
margin: 0 0 units(1);
|
||||
}
|
||||
|
||||
.sms-message-status {
|
||||
@@ -62,18 +62,6 @@
|
||||
margin: -20px units(1) 20px units(1);
|
||||
}
|
||||
|
||||
h2.sms-message-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.usa-prose >*+ h2.message-header {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
h2.recipient-list {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.sms-message-status-outbound {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -143,7 +131,7 @@ h2.recipient-list {
|
||||
&-label,
|
||||
&-button-label {
|
||||
font-weight: bold;
|
||||
font-size: 19px;
|
||||
font-size: 19px;
|
||||
display: block;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
@@ -344,3 +332,11 @@ h2.recipient-list {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.js-focus-style {
|
||||
outline: 3px solid color("blue-40v");
|
||||
box-shadow: 0 0 0 7px color("blue-40v");
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,45 +22,17 @@ i.e.
|
||||
|
||||
@use "uswds-core" as *;
|
||||
|
||||
iframe:focus, [href]:focus, [tabindex]:focus, [contentEditable=true]:focus, button:not([disabled]):focus {
|
||||
outline: units(2px) solid color('blue-40v');
|
||||
outline-offset: 0.3rem;
|
||||
}
|
||||
|
||||
.usa-header--extended {
|
||||
.usa-logo {
|
||||
font-family: family("sans");
|
||||
margin: units(3) 0;
|
||||
@include at-media-max('desktop') {
|
||||
margin: units(4) 0 units(4) units(2);
|
||||
}
|
||||
a {
|
||||
display: flex;
|
||||
img {
|
||||
height: 35px;
|
||||
@include at-media('desktop') {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.usa-header--extended .usa-logo {
|
||||
font-family: family("sans");
|
||||
margin: units(4) 0;
|
||||
@include at-media-max('mobile-lg') {
|
||||
margin: units(4) 0 units(4) units(2);
|
||||
}
|
||||
.usa-nav__secondary {
|
||||
.usa-nav__link {
|
||||
padding: 0;
|
||||
&.usa-current {
|
||||
text-decoration: underline;
|
||||
}
|
||||
img {
|
||||
@include at-media($theme-header-min-width) {
|
||||
width: 80px;
|
||||
height: 70px;
|
||||
}
|
||||
@include at-media-max('desktop') {
|
||||
padding: 0 units(2);
|
||||
ul li {
|
||||
padding-bottom: units(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.usa-nav-container {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,23 +42,21 @@ iframe:focus, [href]:focus, [tabindex]:focus, [contentEditable=true]:focus, butt
|
||||
|
||||
.usa-nav__primary {
|
||||
&> .usa-nav__primary-item > a {
|
||||
font-size: size("body", 5);
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
font-size: size("body", 4);
|
||||
}
|
||||
&> .usa-nav__primary-item:last-child {
|
||||
margin-left: auto;
|
||||
@include u-margin-right(-4);
|
||||
}
|
||||
}
|
||||
|
||||
.usa-nav__primary
|
||||
|
||||
|
||||
h1 {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.usa-section--dark h1 {
|
||||
color: color('blue-30v');
|
||||
@include at-media-max('desktop') {
|
||||
font-size: size("body", 12);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
.list li {
|
||||
font-family: family("sans");
|
||||
@@ -144,69 +114,12 @@ td.table-empty-message {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.button-flex-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: units(2);
|
||||
@include at-media(desktop) {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-button img {
|
||||
margin-left: .5rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.usa-button.login-button.login-button--primary,.login-button.login-button--primary:hover{
|
||||
color:#112e51;background-color:#fff;
|
||||
border:1px solid #767676;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-list-edit-link:active:before,
|
||||
.user-list-edit-link:focus:before {
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.user-list-item {
|
||||
@include at-media(desktop) {
|
||||
width: calc(50% - units(1));
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.user-list-item-heading .live-search-relevant {
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
border: 1px solid color('gray-cool-10');
|
||||
padding: units(2);
|
||||
|
||||
.tick-cross-list-permissions {
|
||||
margin: units(1) 0;
|
||||
padding-left: units(2);
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
font-size: size("body", "sm");
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.template-list-item-without-ancestors .template-list-folder:active,
|
||||
.template-list-item-without-ancestors .template-list-folder:active::before,
|
||||
.template-list-item-without-ancestors .template-list-folder:focus,
|
||||
@@ -219,15 +132,11 @@ td.table-empty-message {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.template-list-item-hidden-by-default {
|
||||
display: none
|
||||
}
|
||||
|
||||
.folder-heading a.folder-heading-folder,
|
||||
.template-list-folder {
|
||||
background: url(../img/material-icons/folder.svg) no-repeat;
|
||||
padding-left: units(4);
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
background-position: 0;
|
||||
}
|
||||
|
||||
@@ -257,7 +166,7 @@ td.table-empty-message {
|
||||
.template-list-template {
|
||||
background: url(../img/material-icons/description.svg) no-repeat;
|
||||
padding-left: units(4);
|
||||
display: inline-flex;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.js-enabled .live-search {
|
||||
@@ -276,12 +185,6 @@ td.table-empty-message {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.usa-table {
|
||||
th {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.js-stick-at-bottom-when-scrolling {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -311,7 +214,7 @@ td.table-empty-message {
|
||||
}
|
||||
|
||||
.usa-hero {
|
||||
background-image: none;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.usa-prose {
|
||||
@@ -329,51 +232,47 @@ td.table-empty-message {
|
||||
}
|
||||
|
||||
.navigation-service.usa-breadcrumb {
|
||||
bottom: 0;
|
||||
-bottom: 0;
|
||||
}
|
||||
|
||||
.table-overflow-x-auto {
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: units('desktop-lg')) {
|
||||
.table-overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
table {
|
||||
min-width: 768px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Dashboard
|
||||
|
||||
.dashboard {
|
||||
.big-number-with-status {
|
||||
a {
|
||||
background: color("blue-60v");
|
||||
padding: units(2);
|
||||
margin-bottom: units(1);
|
||||
text-decoration: none;
|
||||
&:hover{
|
||||
background: color("blue-warm-70v");
|
||||
}
|
||||
}
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
.big-number-smaller {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.big-number-number {
|
||||
font-size: units(5);
|
||||
line-height: units(5);
|
||||
line-height: units(6);
|
||||
}
|
||||
.big-number-label {
|
||||
font-size: units(2.5);
|
||||
font-size: units(3);
|
||||
}
|
||||
}
|
||||
.big-number-status {
|
||||
background: color("green-cool-40v");
|
||||
display: flex;
|
||||
display: flex;
|
||||
padding: units(1) units(2);
|
||||
&--failing {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
a.usa-link {
|
||||
color: white;
|
||||
background: color("red-warm-50v");
|
||||
padding: units(1) units(2);
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background: color("red-warm-60v");
|
||||
@@ -383,12 +282,12 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
.usa-table {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
caption {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table-field-center-aligned {
|
||||
text-align: center;
|
||||
text-align: center;
|
||||
}
|
||||
.template-statistics-table-template-name {
|
||||
padding-left: units(4);
|
||||
@@ -401,146 +300,63 @@ td.table-empty-message {
|
||||
|
||||
.dashboard-table {
|
||||
table {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.file-list-filename {
|
||||
font-weight: bold;
|
||||
}
|
||||
.file-list-hint {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
}
|
||||
.table-field, .table-field-right-aligned {
|
||||
width: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
&.usage-table {
|
||||
.table-field, .table-field-left-aligned, .table-field-right-aligned {
|
||||
width: auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.job-status-table {
|
||||
table-layout: fixed;
|
||||
|
||||
thead tr th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
th:nth-child(2),R
|
||||
td:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.usage-table {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.big-number-smallest {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.job-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
td.file-name {
|
||||
width: 25%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
td.jobid {
|
||||
width: 5%;
|
||||
}
|
||||
td.template {
|
||||
width: 25%;
|
||||
}
|
||||
td.time-sent {
|
||||
width: 15%;
|
||||
}
|
||||
td.sender {
|
||||
width: 20%;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
td.count-of-recipients {
|
||||
width: 5%;
|
||||
}
|
||||
td.report {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.delivered {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.failed {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
}
|
||||
td.report img {
|
||||
padding-top: 5px;
|
||||
}
|
||||
th {
|
||||
padding: 0.5rem 1rem
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.usa-table-container--scrollable-mobile {
|
||||
margin: 0;
|
||||
overflow-y:hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-table th[data-sortable][aria-sort=ascending], .usa-table th[data-sortable][aria-sort=descending] {
|
||||
background-color: #a1d3ff;
|
||||
}
|
||||
|
||||
#template-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: units(1) 0 units(1) units(1);
|
||||
margin: units(2) 0 units(5);
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-prose > p.max-width-full {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs
|
||||
|
||||
.tabs {
|
||||
.pill {
|
||||
display: flex;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
.pill-item__container {
|
||||
border: 1px solid color("gray-cool-10");
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
font-size: units(2);
|
||||
a {
|
||||
a {
|
||||
padding: units(4);
|
||||
.big-number-smaller {
|
||||
font-size: units(5);
|
||||
@@ -551,7 +367,7 @@ td.table-empty-message {
|
||||
}
|
||||
&:not(.pill-item--selected):hover {
|
||||
background: color("blue-warm-70v");
|
||||
}
|
||||
}
|
||||
&.pill-item--selected:hover {
|
||||
color: color("blue-60v");
|
||||
}
|
||||
@@ -563,12 +379,12 @@ td.table-empty-message {
|
||||
// Etc
|
||||
|
||||
.email-brand, .browse-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
margin-bottom: units(2);
|
||||
li {
|
||||
padding: 8px 0;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +392,7 @@ details form {
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
// Textbox highlight
|
||||
// Textbox highlight
|
||||
|
||||
.textbox-highlight {
|
||||
|
||||
@@ -624,7 +440,7 @@ details form {
|
||||
.placeholder,
|
||||
.placeholder-conditional {
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
position: relative;
|
||||
&:after {
|
||||
content: "";
|
||||
background-color: color("yellow-20v");
|
||||
@@ -633,9 +449,9 @@ details form {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
right: 6px;
|
||||
right: 6px;
|
||||
height: 100%;
|
||||
border-radius: 7px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,245 +471,3 @@ details form {
|
||||
padding-left: 5px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.edit-textbox-error-mt {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
// Login page
|
||||
|
||||
#countdown-container {
|
||||
display: none; // Hide the countdown timer
|
||||
}
|
||||
|
||||
.placeholder, .placeholder-conditional {
|
||||
background-color: #face00;
|
||||
border: 1px solid #face00;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.usa-prose.margin-top-0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ol.best-practices-list {
|
||||
counter-reset: item;
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
ol.best-practices-list.set-two {
|
||||
counter-reset: item 6;
|
||||
}
|
||||
|
||||
ol.best-practices-list.set-three {
|
||||
counter-reset: item 10;
|
||||
}
|
||||
|
||||
ol.best-practices-list li {
|
||||
counter-increment: item;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
ol.best-practices-list li::before {
|
||||
content: counter(item);
|
||||
background-color: #005ea2;
|
||||
color: white;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
li.best-practices {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
div.best-practices {
|
||||
height: 400px
|
||||
}
|
||||
|
||||
$do-dont-color-do: "green-cool-50v";
|
||||
$do-dont-color-dont: "red-cool-50v";
|
||||
$do-dont-color-border: "gray-cool-20";
|
||||
$do-dont-padding: 3;
|
||||
$do-dont-top-bar-width: 1;
|
||||
|
||||
.do-dont {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding-top: units(2);
|
||||
}
|
||||
|
||||
.do-dont__do,
|
||||
.do-dont__dont {
|
||||
background-color: color("white");
|
||||
border: 1px solid color($do-dont-color-border);
|
||||
flex: 1 0 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.do-dont__do {
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
.do-dont__do:before,
|
||||
.do-dont__dont:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: units($do-dont-top-bar-width);
|
||||
width: calc(100% + 2px);
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.do-dont__do:before {
|
||||
background-color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont:before {
|
||||
background-color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__heading {
|
||||
// @include typeset("lang", "lg", 2);
|
||||
align-items: center;
|
||||
border-bottom: 1px solid color($do-dont-color-border);
|
||||
display: flex;
|
||||
font-weight: fw("bold");
|
||||
margin: 0;
|
||||
padding: units(4) units($do-dont-padding) units($do-dont-padding);
|
||||
|
||||
.usa-icon {
|
||||
@include u-square(3);
|
||||
margin-right: units(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.do-dont__do .do-dont__heading {
|
||||
color: color($do-dont-color-do);
|
||||
}
|
||||
|
||||
.do-dont__dont .do-dont__heading {
|
||||
color: color($do-dont-color-dont);
|
||||
}
|
||||
|
||||
.do-dont__content {
|
||||
padding: 0 units($do-dont-padding) units($do-dont-padding) units($do-dont-padding);
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
li+li {
|
||||
margin-top: units(1);
|
||||
}
|
||||
}
|
||||
|
||||
.site-note {
|
||||
background-color: #dfe1e2;
|
||||
max-width: 72ex;
|
||||
padding: .5rem;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.svg-circle-icon {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: text-bottom;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.height-phone-lg {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 758px) {
|
||||
.best-practices-flex-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-content:hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.3s ease, background-color 0.3s ease,
|
||||
}
|
||||
|
||||
li.linked-card:hover > div:first-child:hover {
|
||||
border-color: #005ea2;
|
||||
}
|
||||
|
||||
li.linked-card:hover h4,
|
||||
li.linked-card:hover p,
|
||||
li.linked-card:hover svg,
|
||||
.best-practices_card_img:hover{
|
||||
color: #005ea2;
|
||||
}
|
||||
|
||||
.best-practices_card_img {
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.best-practices_link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.usa-link--downloadable {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.usa-link--downloadable::after {
|
||||
background-image: url(../img/material-icons/download.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: 1.75ex 1.75ex;
|
||||
height: 1.75ex;
|
||||
width: 1.75ex;
|
||||
content: "";
|
||||
display: inline;
|
||||
margin-top: .7ex;
|
||||
margin-left: 2px;
|
||||
padding-left: 1.75ex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.usa-link--downloadable::after {
|
||||
background: 0 0;
|
||||
background-color: currentColor;
|
||||
-webkit-mask-image: url(../img/material-icons/download.svg), linear-gradient(transparent, transparent);
|
||||
mask-image: url(../img/material-icons/download.svg), linear-gradient(transparent, transparent);
|
||||
-webkit-mask-position: center center;
|
||||
mask-position: center center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 1.75ex 1.75ex;
|
||||
mask-size: 1.75ex 1.75ex;
|
||||
}
|
||||
|
||||
.usa-sidenav__sublist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.usa-sidenav__item:hover .usa-sidenav__sublist,
|
||||
.usa-sidenav__item:focus-within .usa-sidenav__sublist {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.usa-sidenav__item a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,5 @@ in the form $setting: value,
|
||||
|
||||
@use "uswds-core" with (
|
||||
$theme-font-type-sans: "public-sans",
|
||||
$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-show-notifications: false
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@forward "uswds-theme";
|
||||
@forward "uswds";
|
||||
@forward "uswds-theme-custom-styles";
|
||||
@forward "legacy-styles";
|
||||
@forward "data-visualization";
|
||||
@forward "legacy-styles";
|
||||
@@ -34,11 +34,6 @@ $path: '/static/images/';
|
||||
// Dependencies from GOV.UK Frontend, packaged to be specific to this application
|
||||
@import './govuk-frontend/all';
|
||||
|
||||
// Custom overrides
|
||||
.govuk-link {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Specific to this application
|
||||
@import 'local/typography';
|
||||
@import 'grids';
|
||||
|
||||
@@ -2,13 +2,12 @@ import json
|
||||
from os import getenv
|
||||
|
||||
import newrelic.agent
|
||||
from notifications_utils import DAILY_MESSAGE_LIMIT
|
||||
|
||||
from app.cloudfoundry_config import cloud_config
|
||||
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")
|
||||
@@ -37,9 +36,13 @@ class Config(object):
|
||||
NR_BROWSER_KEY = getenv("NR_BROWSER_KEY")
|
||||
settings = newrelic.agent.global_settings()
|
||||
NR_MONITOR_ON = settings and settings.monitor_mode
|
||||
COMMIT_HASH = getenv("COMMIT_HASH", "--------")[0:7]
|
||||
|
||||
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov", "mil", "si.edu"]
|
||||
TEMPLATE_PREVIEW_API_HOST = getenv(
|
||||
"TEMPLATE_PREVIEW_API_HOST", "http://localhost:9999"
|
||||
)
|
||||
TEMPLATE_PREVIEW_API_KEY = getenv("TEMPLATE_PREVIEW_API_KEY", "my-secret-key")
|
||||
|
||||
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov"]
|
||||
|
||||
# Logging
|
||||
NOTIFY_LOG_LEVEL = getenv("NOTIFY_LOG_LEVEL", "INFO")
|
||||
@@ -54,13 +57,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
|
||||
@@ -84,12 +81,10 @@ class Config(object):
|
||||
"IBAN": "GB33BUKB20201555555555",
|
||||
"swift": "ABCDEF12",
|
||||
"notify_billing_email_addresses": [
|
||||
"tts-benefits-studio@gsa.gov",
|
||||
"generic@digital.cabinet-office.gov.uk",
|
||||
"first.last@digital.cabinet-office.gov.uk",
|
||||
],
|
||||
}
|
||||
FEATURE_BEST_PRACTICES_ENABLED = (
|
||||
getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true"
|
||||
)
|
||||
|
||||
|
||||
def _s3_credentials_from_env(bucket_prefix):
|
||||
@@ -157,34 +152,6 @@ class Staging(Production):
|
||||
HEADER_COLOUR = "#00ff00" # $green
|
||||
|
||||
|
||||
class E2ETest(Staging):
|
||||
"""
|
||||
An environment config that is intended to operate as if it were in the
|
||||
staging environment but with the configuration of the development and test
|
||||
environments so the E2E tests work.
|
||||
"""
|
||||
|
||||
# Borrowed from development environment
|
||||
DEBUG = True
|
||||
SESSION_COOKIE_SECURE = False
|
||||
SESSION_PROTECTION = None
|
||||
HTTP_PROTOCOL = "http"
|
||||
ASSET_DOMAIN = ""
|
||||
ASSET_PATH = "/static/"
|
||||
|
||||
# Borrowed from test environment
|
||||
TESTING = True
|
||||
WTF_CSRF_ENABLED = False
|
||||
|
||||
# buckets - mirror staging
|
||||
CSV_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
||||
"notify-api-csv-upload-bucket-staging"
|
||||
)
|
||||
LOGO_UPLOAD_BUCKET = cloud_config.s3_credentials(
|
||||
"notify-admin-logo-upload-bucket-staging"
|
||||
)
|
||||
|
||||
|
||||
class Demo(Staging):
|
||||
HEADER_COLOUR = "#6F72AF" # $mauve
|
||||
|
||||
@@ -206,7 +173,6 @@ class Scanning(Production):
|
||||
configs = {
|
||||
"development": Development,
|
||||
"test": Test,
|
||||
"e2etest": E2ETest,
|
||||
"scanning": Scanning,
|
||||
"staging": Staging,
|
||||
"demo": Demo,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Get started
|
||||
|
||||
|
||||
1. ## Everyone starts in [Trial Mode](/using-notify/trial-mode)
|
||||
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/guidance) to increase response.
|
||||
|
||||
3. ## Check delivery status
|
||||
[Analyze the delivery](/using-notify/delivery-status) of your messages and download reports
|
||||
|
||||
4. ## View messaging limits
|
||||
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>.
|
||||
We’ll respond within one business day.
|
||||
|
||||
### Questions?
|
||||
[Contact the Notify team](/support)
|
||||
@@ -24,7 +24,6 @@ EVENT_SCHEMAS = {
|
||||
"service_id",
|
||||
"ui_permissions",
|
||||
},
|
||||
"resend_user_invite_to_service": {"email_address", "resent_by_id", "service_id"},
|
||||
"cancel_user_invite_to_service": {"email_address", "canceled_by_id", "service_id"},
|
||||
"set_user_permissions": {
|
||||
"user_id",
|
||||
@@ -64,10 +63,6 @@ def create_cancel_user_invite_to_service_event(**kwargs):
|
||||
_send_event("cancel_user_invite_to_service", **kwargs)
|
||||
|
||||
|
||||
def create_resend_user_invite_to_service_event(**kwargs):
|
||||
_send_event("resend_user_invite_to_service", **kwargs)
|
||||
|
||||
|
||||
def create_add_user_to_service_event(**kwargs):
|
||||
_send_event("add_user_to_service", **kwargs)
|
||||
|
||||
|
||||
@@ -1,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()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
import urllib
|
||||
@@ -10,55 +9,15 @@ from numbers import Number
|
||||
import ago
|
||||
import dateutil
|
||||
import humanize
|
||||
import markdown
|
||||
import pytz
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import render_template_string, url_for
|
||||
from flask.helpers import get_root_path
|
||||
from markupsafe import Markup
|
||||
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
from app.utils.time import parse_naive_dt
|
||||
from flask import Markup, url_for
|
||||
from notifications_utils.field import Field
|
||||
from notifications_utils.formatters import make_quotes_smart
|
||||
from notifications_utils.formatters import nl2br as utils_nl2br
|
||||
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
|
||||
from notifications_utils.take import Take
|
||||
|
||||
|
||||
def apply_html_class(tags, html_file):
|
||||
new_html = html_file
|
||||
|
||||
for tag in tags:
|
||||
element = tag[0]
|
||||
class_name = tag[1]
|
||||
|
||||
soup = BeautifulSoup(new_html, "html.parser")
|
||||
|
||||
for xtag in soup.find_all(element):
|
||||
xtag["class"] = class_name
|
||||
|
||||
new_html = str(soup)
|
||||
|
||||
return new_html
|
||||
|
||||
|
||||
def convert_markdown_template(mdf, test=False):
|
||||
content_text = ""
|
||||
|
||||
if not test:
|
||||
APP_ROOT = get_root_path("notifications-admin")
|
||||
file = "app/content/" + mdf + ".md"
|
||||
md_file = os.path.join(APP_ROOT, file)
|
||||
with open(md_file) as f:
|
||||
content_text = f.read()
|
||||
else:
|
||||
content_text = mdf
|
||||
|
||||
jn_render = render_template_string(content_text)
|
||||
md_render = markdown.markdown(jn_render)
|
||||
|
||||
return md_render
|
||||
from app.utils.time import parse_naive_dt
|
||||
|
||||
|
||||
def convert_to_boolean(value):
|
||||
@@ -72,14 +31,13 @@ def convert_to_boolean(value):
|
||||
|
||||
|
||||
def format_datetime(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
return "{} at {} UTC".format(format_date(date), format_time_24h(date))
|
||||
|
||||
|
||||
def format_datetime_24h(date):
|
||||
return "{} at {} {}".format(
|
||||
format_date(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
return "{} at {} UTC".format(
|
||||
format_date(date),
|
||||
format_time_24h(date),
|
||||
)
|
||||
|
||||
|
||||
@@ -88,77 +46,39 @@ def format_time(date):
|
||||
|
||||
|
||||
def format_datetime_normal(date):
|
||||
# example: February 20, 2024 at 07:00 PM US/Eastern, used for datetimes that's not within tables
|
||||
return "{} at {} {}".format(
|
||||
format_date_normal(date), format_time_12h(date), get_user_preferred_timezone()
|
||||
)
|
||||
return "{} at {} UTC".format(format_date_normal(date), format_time_24h(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)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%I:%M %p")
|
||||
)
|
||||
def format_datetime_short(date):
|
||||
return "{} at {} UTC".format(format_date_short(date), format_time_24h(date))
|
||||
|
||||
|
||||
def format_datetime_relative(date):
|
||||
return "{} at {} {}".format(
|
||||
get_human_day(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
)
|
||||
return "{} at {} UTC".format(get_human_day(date), format_time_24h(date))
|
||||
|
||||
|
||||
def format_datetime_numeric(date):
|
||||
return "{} {} {}".format(
|
||||
format_date_numeric(date), format_time_24h(date), get_user_preferred_timezone()
|
||||
return "{} {} UTC".format(
|
||||
format_date_numeric(date),
|
||||
format_time_24h(date),
|
||||
)
|
||||
|
||||
|
||||
def format_date_numeric(date):
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%m-%d-%Y")
|
||||
)
|
||||
return date.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def format_time_24h(date):
|
||||
date = parse_naive_dt(date)
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%H:%M")
|
||||
return date.strftime("%H:%M")
|
||||
|
||||
|
||||
def get_human_day(time, date_prefix=""):
|
||||
# Add 1 minute to transform 00:00 into ‘midnight today’ instead of ‘midnight tomorrow’
|
||||
time = parse_naive_dt(time)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
time = time.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
|
||||
date = (time - timedelta(minutes=1)).date()
|
||||
now = datetime.now(preferred_tz)
|
||||
now = datetime.now(pytz.utc)
|
||||
|
||||
if date == (now + timedelta(days=1)).date():
|
||||
return "tomorrow"
|
||||
@@ -180,25 +100,17 @@ def get_human_day(time, date_prefix=""):
|
||||
|
||||
def format_date(date):
|
||||
date = parse_naive_dt(date)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
date.replace(tzinfo=timezone.utc)
|
||||
.astimezone(preferred_tz)
|
||||
.strftime("%A %d %B %Y")
|
||||
)
|
||||
return date.strftime("%A %d %B %Y")
|
||||
|
||||
|
||||
def format_date_normal(date):
|
||||
date = parse_naive_dt(date)
|
||||
return date.strftime("%B %d, %Y").lstrip("0")
|
||||
return date.strftime("%d %B %Y").lstrip("0")
|
||||
|
||||
|
||||
def format_date_short(date):
|
||||
date = parse_naive_dt(date)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return _format_datetime_short(
|
||||
date.replace(tzinfo=timezone.utc).astimezone(preferred_tz)
|
||||
)
|
||||
return _format_datetime_short(date)
|
||||
|
||||
|
||||
def format_date_human(date):
|
||||
@@ -206,17 +118,15 @@ def format_date_human(date):
|
||||
|
||||
|
||||
def format_datetime_human(date, date_prefix=""):
|
||||
return "{} at {} {}".format(
|
||||
return "{} at {} UTC".format(
|
||||
get_human_day(date, date_prefix="on"),
|
||||
format_time_24h(date),
|
||||
get_user_preferred_timezone(),
|
||||
)
|
||||
|
||||
|
||||
def format_day_of_week(date):
|
||||
date = parse_naive_dt(date)
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return date.replace(tzinfo=timezone.utc).astimezone(preferred_tz).strftime("%A")
|
||||
return date.strftime("%A")
|
||||
|
||||
|
||||
def _format_datetime_short(datetime):
|
||||
@@ -231,11 +141,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)
|
||||
@@ -479,12 +384,6 @@ def message_count_noun(count, template_type):
|
||||
else:
|
||||
return "text messages"
|
||||
|
||||
if template_type == "parts":
|
||||
if count == 1:
|
||||
return "text message part"
|
||||
else:
|
||||
return "text message parts"
|
||||
|
||||
elif template_type == "email":
|
||||
if count == 1:
|
||||
return "email"
|
||||
|
||||
@@ -3,13 +3,13 @@ from flask import Blueprint
|
||||
main = Blueprint("main", __name__)
|
||||
|
||||
from app.main.views import ( # noqa isort:skip
|
||||
activity,
|
||||
add_service,
|
||||
api_keys,
|
||||
choose_account,
|
||||
code_not_received,
|
||||
conversation,
|
||||
dashboard,
|
||||
email_branding,
|
||||
feedback,
|
||||
find_services,
|
||||
find_users,
|
||||
@@ -26,6 +26,7 @@ from app.main.views import ( # noqa isort:skip
|
||||
performance,
|
||||
platform_admin,
|
||||
pricing,
|
||||
providers,
|
||||
register,
|
||||
security_policy,
|
||||
send,
|
||||
|
||||
@@ -4,13 +4,15 @@ from itertools import chain
|
||||
from numbers import Number
|
||||
|
||||
import pytz
|
||||
from flask import render_template, request
|
||||
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 notifications_utils.formatters import strip_all_whitespace
|
||||
from notifications_utils.insensitive_dict import InsensitiveDict
|
||||
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
|
||||
from werkzeug.utils import cached_property
|
||||
from wtforms import (
|
||||
BooleanField,
|
||||
@@ -50,7 +52,6 @@ from app.main.validators import (
|
||||
CommonlyUsedPassword,
|
||||
CsvFileValidator,
|
||||
DoesNotStartWithDoubleZero,
|
||||
FieldCannotContainComma,
|
||||
LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly,
|
||||
MustContainAlphanumericCharacters,
|
||||
NoCommasInPlaceHolders,
|
||||
@@ -60,23 +61,18 @@ from app.main.validators import (
|
||||
ValidEmail,
|
||||
ValidGovEmail,
|
||||
)
|
||||
from app.models.feedback import PROBLEM_TICKET_TYPE, QUESTION_TICKET_TYPE
|
||||
from app.models.organization import Organization
|
||||
from app.utils import merge_jsonlike
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
from app.utils import branding, merge_jsonlike
|
||||
from app.utils.user_permissions import all_ui_permissions, permission_options
|
||||
from notifications_utils.formatters import strip_all_whitespace
|
||||
from notifications_utils.insensitive_dict import InsensitiveDict
|
||||
from notifications_utils.recipients import InvalidPhoneError, validate_phone_number
|
||||
|
||||
|
||||
def get_time_value_and_label(future_time):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (
|
||||
future_time.astimezone(preferred_tz).isoformat(),
|
||||
"{} at {} {}".format(
|
||||
get_human_day(future_time.astimezone(preferred_tz)),
|
||||
get_human_time(future_time.astimezone(preferred_tz)),
|
||||
get_user_preferred_timezone(),
|
||||
future_time.astimezone(pytz.utc).replace(tzinfo=None).isoformat(),
|
||||
"{} at {} UTC".format(
|
||||
get_human_day(future_time.astimezone(pytz.utc)),
|
||||
get_human_time(future_time.astimezone(pytz.utc)),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -89,25 +85,21 @@ def get_human_time(time):
|
||||
|
||||
def get_human_day(time, prefix_today_with="T"):
|
||||
# Add 1 hour to get ‘midnight today’ instead of ‘midnight tomorrow’
|
||||
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
time = (time - timedelta(hours=1)).strftime("%A")
|
||||
if time == datetime.now(preferred_tz).strftime("%A"):
|
||||
if time == datetime.now(pytz.utc).strftime("%A"):
|
||||
return "{}oday".format(prefix_today_with)
|
||||
if time == (datetime.now(preferred_tz) + timedelta(days=1)).strftime("%A"):
|
||||
if time == (datetime.now(pytz.utc) + timedelta(days=1)).strftime("%A"):
|
||||
return "Tomorrow"
|
||||
return time
|
||||
|
||||
|
||||
def get_furthest_possible_scheduled_time():
|
||||
# We want local time to find date boundaries
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
return (datetime.now(preferred_tz) + timedelta(days=4)).replace(hour=0)
|
||||
return (datetime.now(pytz.utc) + timedelta(days=4)).replace(hour=0)
|
||||
|
||||
|
||||
def get_next_hours_until(until):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
now = datetime.now(preferred_tz)
|
||||
now = datetime.now(pytz.utc)
|
||||
hours = int((until - now).total_seconds() / (60 * 60))
|
||||
return [
|
||||
(now + timedelta(hours=i)).replace(minute=0, second=0, microsecond=0)
|
||||
@@ -116,8 +108,7 @@ def get_next_hours_until(until):
|
||||
|
||||
|
||||
def get_next_days_until(until):
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
now = datetime.now(preferred_tz)
|
||||
now = datetime.now(pytz.utc)
|
||||
days = int((until - now).total_seconds() / (60 * 60 * 24))
|
||||
return [
|
||||
get_human_day((now + timedelta(days=i)), prefix_today_with="Later t")
|
||||
@@ -607,15 +598,6 @@ class RegisterUserForm(StripWhitespaceForm):
|
||||
auth_type = HiddenField("auth_type", default="sms_auth")
|
||||
|
||||
|
||||
class SetupUserProfileForm(StripWhitespaceForm):
|
||||
name = GovukTextInputField(
|
||||
"Full name", validators=[DataRequired(message="Cannot be empty")]
|
||||
)
|
||||
mobile_number = international_phone_number()
|
||||
# TODO This should be replaced with a select widget when one is available.
|
||||
preferred_timezone = HiddenField("preferred_timezone", default="US/Eastern")
|
||||
|
||||
|
||||
class RegisterUserFromInviteForm(RegisterUserForm):
|
||||
def __init__(self, invited_user):
|
||||
super().__init__(
|
||||
@@ -825,6 +807,49 @@ class GovukCheckboxField(BooleanField):
|
||||
)
|
||||
|
||||
|
||||
class GovukTextareaField(TextAreaField):
|
||||
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
|
||||
super(TextAreaField, self).__init__(label, validators, **kwargs)
|
||||
self.param_extensions = param_extensions
|
||||
|
||||
# self.__call__ renders the HTML for the field by:
|
||||
# 1. delegating to self.meta.render_field which
|
||||
# 2. calls field.widget
|
||||
# this bypasses that by making self.widget a method with the same interface as widget.__call__
|
||||
def widget(self, field, param_extensions=None, **kwargs):
|
||||
# error messages
|
||||
error_message = None
|
||||
if field.errors:
|
||||
error_message = {"text": field.errors[0]}
|
||||
|
||||
params = {
|
||||
"name": field.name,
|
||||
"id": field.id,
|
||||
"rows": 8,
|
||||
"label": {
|
||||
"text": field.label.text,
|
||||
"classes": None,
|
||||
"isPageHeading": False,
|
||||
},
|
||||
"hint": {"text": None},
|
||||
"errorMessage": error_message,
|
||||
}
|
||||
|
||||
# extend default params with any sent in during instantiation
|
||||
if self.param_extensions:
|
||||
merge_jsonlike(params, self.param_extensions)
|
||||
|
||||
# add any sent in though use in templates
|
||||
if param_extensions:
|
||||
merge_jsonlike(params, param_extensions)
|
||||
|
||||
return Markup(
|
||||
render_template(
|
||||
"components/components/textarea/template.njk", params=params
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
|
||||
class GovukCheckboxesField(SelectMultipleField):
|
||||
render_as_list = False
|
||||
@@ -923,9 +948,9 @@ class OnOffField(GovukRadiosField):
|
||||
]
|
||||
super().__init__(
|
||||
label,
|
||||
*args,
|
||||
choices=choices,
|
||||
thing=f"{choices[0][1].lower()} or {choices[1][1].lower()}",
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -1163,7 +1188,7 @@ class AdminServiceSMSAllowanceForm(StripWhitespaceForm):
|
||||
|
||||
class AdminServiceMessageLimitForm(StripWhitespaceForm):
|
||||
message_limit = GovukIntegerField(
|
||||
"Max number of messages the service has per send",
|
||||
"Number of messages the service is allowed to send each day",
|
||||
validators=[DataRequired(message="Cannot be empty")],
|
||||
)
|
||||
|
||||
@@ -1237,11 +1262,7 @@ class CsvUploadForm(StripWhitespaceForm):
|
||||
validators=[
|
||||
DataRequired(message="Please pick a file"),
|
||||
CsvFileValidator(),
|
||||
FileSize(
|
||||
max_size=10e6,
|
||||
message="File must be smaller than 10Mb. If you are trying to upload an Excel file, \
|
||||
please export the contents in the CSV format and then try again.",
|
||||
), # 10Mb
|
||||
FileSize(max_size=10e6, message="File must be smaller than 10Mb"), # 10Mb
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1250,27 +1271,6 @@ class ChangeNameForm(StripWhitespaceForm):
|
||||
new_name = GovukTextInputField("Your name")
|
||||
|
||||
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
class ChangePreferredTimezoneForm(StripWhitespaceForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChangePreferredTimezoneForm, self).__init__(*args, **kwargs)
|
||||
self.new_preferred_timezone.choices = [
|
||||
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||
("US/Eastern", "US/Eastern"),
|
||||
("US/Central", "US/Central"),
|
||||
("US/Mountain", "US/Mountain"),
|
||||
("US/Pacific", "US/Pacific"),
|
||||
("US/Alaska", "US/Alaska"),
|
||||
("US/Hawaii", "US/Hawaii"),
|
||||
("US/Aleutian", "US/Aleutian"),
|
||||
("US/Samoa", "US/Samoa"),
|
||||
]
|
||||
|
||||
new_preferred_timezone = GovukRadiosField(
|
||||
"What timezone would you like to use?",
|
||||
)
|
||||
|
||||
|
||||
class ChangeEmailForm(StripWhitespaceForm):
|
||||
def __init__(self, validate_email_func, *args, **kwargs):
|
||||
self.validate_email_func = validate_email_func
|
||||
@@ -1337,6 +1337,49 @@ class CreateKeyForm(StripWhitespaceForm):
|
||||
raise ValidationError("A key with this name already exists")
|
||||
|
||||
|
||||
class SupportType(StripWhitespaceForm):
|
||||
support_type = GovukRadiosField(
|
||||
"How can we help you?",
|
||||
choices=[
|
||||
(PROBLEM_TICKET_TYPE, "Report a problem"),
|
||||
(QUESTION_TICKET_TYPE, "Ask a question or give feedback"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class SupportRedirect(StripWhitespaceForm):
|
||||
who = GovukRadiosField(
|
||||
"What do you need help with?",
|
||||
choices=[
|
||||
(
|
||||
"public-sector",
|
||||
"I work in the public sector and need to send emails or text messages",
|
||||
),
|
||||
("public", "I’m a member of the public with a question for the government"),
|
||||
],
|
||||
param_extensions={"fieldset": {"legend": {"classes": "usa-sr-only"}}},
|
||||
)
|
||||
|
||||
|
||||
class FeedbackOrProblem(StripWhitespaceForm):
|
||||
name = GovukTextInputField("Name (optional)")
|
||||
email_address = email_address(label="Email address", gov_user=False, required=True)
|
||||
feedback = TextAreaField(
|
||||
"Your message", validators=[DataRequired(message="Cannot be empty")]
|
||||
)
|
||||
|
||||
|
||||
class Triage(StripWhitespaceForm):
|
||||
severe = GovukRadiosField(
|
||||
"Is it an emergency?",
|
||||
choices=[
|
||||
("yes", "Yes"),
|
||||
("no", "No"),
|
||||
],
|
||||
thing="yes or no",
|
||||
)
|
||||
|
||||
|
||||
class EstimateUsageForm(StripWhitespaceForm):
|
||||
volume_email = ForgivingIntegerField(
|
||||
"How many emails do you expect to send in the next year?",
|
||||
@@ -1372,6 +1415,52 @@ class EstimateUsageForm(StripWhitespaceForm):
|
||||
return super().validate(*args, **kwargs)
|
||||
|
||||
|
||||
class AdminProviderRatioForm(Form):
|
||||
def __init__(self, providers):
|
||||
self._providers = providers
|
||||
|
||||
# hack: https://github.com/wtforms/wtforms/issues/736
|
||||
self._unbound_fields = [
|
||||
(
|
||||
provider["identifier"],
|
||||
GovukIntegerField(
|
||||
f"{provider['display_name']} (%)",
|
||||
validators=[
|
||||
validators.NumberRange(
|
||||
min=0, max=100, message="Must be between 0 and 100"
|
||||
)
|
||||
],
|
||||
param_extensions={
|
||||
"classes": "width-8",
|
||||
},
|
||||
),
|
||||
)
|
||||
for provider in providers
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
data={
|
||||
provider["identifier"]: provider["priority"] for provider in providers
|
||||
}
|
||||
)
|
||||
|
||||
def validate(self, extra_validators=None):
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
total = sum(
|
||||
getattr(self, provider["identifier"]).data for provider in self._providers
|
||||
)
|
||||
|
||||
if total == 100:
|
||||
return True
|
||||
|
||||
for provider in self._providers:
|
||||
getattr(self, provider["identifier"]).errors += ["Must add up to 100%"]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ServiceContactDetailsForm(StripWhitespaceForm):
|
||||
contact_details_type = RadioField(
|
||||
"Type of contact details",
|
||||
@@ -1475,6 +1564,65 @@ class ServiceSwitchChannelForm(ServiceOnOffSettingForm):
|
||||
super().__init__(name, *args, **kwargs)
|
||||
|
||||
|
||||
class AdminSetEmailBrandingForm(StripWhitespaceForm):
|
||||
branding_style = GovukRadiosFieldWithNoneOption(
|
||||
"Branding style",
|
||||
param_extensions={"fieldset": {"legend": {"classes": "usa-sr-only"}}},
|
||||
thing="a branding style",
|
||||
)
|
||||
|
||||
DEFAULT = (FieldWithNoneOption.NONE_OPTION_VALUE, "GOV.UK")
|
||||
|
||||
def __init__(self, all_branding_options, current_branding):
|
||||
super().__init__(branding_style=current_branding)
|
||||
|
||||
self.branding_style.choices = sorted(
|
||||
all_branding_options + [self.DEFAULT],
|
||||
key=lambda branding: (
|
||||
branding[0] != current_branding,
|
||||
branding[0] is not self.DEFAULT[0],
|
||||
branding[1].lower(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AdminPreviewBrandingForm(StripWhitespaceForm):
|
||||
branding_style = HiddenFieldWithNoneOption("branding_style")
|
||||
|
||||
|
||||
class AdminEditEmailBrandingForm(StripWhitespaceForm):
|
||||
name = GovukTextInputField("Name of brand")
|
||||
text = GovukTextInputField("Text")
|
||||
colour = GovukTextInputField(
|
||||
"Colour",
|
||||
validators=[
|
||||
Regexp(
|
||||
regex="^$|^#(?:[0-9a-fA-F]{3}){1,2}$",
|
||||
message="Must be a valid color hex code (starting with #)",
|
||||
)
|
||||
],
|
||||
param_extensions={
|
||||
"attributes": {"data-module": "colour-preview"},
|
||||
},
|
||||
)
|
||||
file = FileField_wtf(
|
||||
"Upload a PNG logo", validators=[FileAllowed(["png"], "PNG Images only!")]
|
||||
)
|
||||
brand_type = GovukRadiosField(
|
||||
"Brand type",
|
||||
choices=[
|
||||
("both", "GOV.UK and branding"),
|
||||
("org", "Branding only"),
|
||||
("org_banner", "Branding banner"),
|
||||
],
|
||||
)
|
||||
|
||||
def validate_name(self, name):
|
||||
op = request.form.get("operation")
|
||||
if op == "email-branding-details" and not self.name.data:
|
||||
raise ValidationError("This field is required")
|
||||
|
||||
|
||||
class SVGFileUpload(StripWhitespaceForm):
|
||||
file = FileField_wtf(
|
||||
"Upload an SVG logo",
|
||||
@@ -1651,11 +1799,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
|
||||
@@ -1693,6 +1837,42 @@ class AdminSetOrganizationForm(StripWhitespaceForm):
|
||||
)
|
||||
|
||||
|
||||
class ChooseBrandingForm(StripWhitespaceForm):
|
||||
FALLBACK_OPTION_VALUE = "something_else"
|
||||
FALLBACK_OPTION = (FALLBACK_OPTION_VALUE, "Something else")
|
||||
|
||||
@property
|
||||
def something_else_is_only_option(self):
|
||||
return self.options.choices == (self.FALLBACK_OPTION,)
|
||||
|
||||
|
||||
class ChooseEmailBrandingForm(ChooseBrandingForm):
|
||||
options = RadioField("Choose your new email branding")
|
||||
|
||||
def __init__(self, service):
|
||||
super().__init__()
|
||||
|
||||
self.options.choices = tuple(
|
||||
list(branding.get_email_choices(service)) + [self.FALLBACK_OPTION]
|
||||
)
|
||||
|
||||
|
||||
class SomethingElseBrandingForm(StripWhitespaceForm):
|
||||
something_else = GovukTextareaField(
|
||||
"Describe the branding you want",
|
||||
validators=[DataRequired("Cannot be empty")],
|
||||
param_extensions={
|
||||
"label": {
|
||||
"isPageHeading": True,
|
||||
"classes": "font-body-xl",
|
||||
},
|
||||
"hint": {
|
||||
"text": "Include links to your brand guidelines or examples of how to use your branding."
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AdminServiceAddDataRetentionForm(StripWhitespaceForm):
|
||||
notification_type = GovukRadiosField(
|
||||
"What notification type?",
|
||||
@@ -1800,16 +1980,12 @@ class TemplateAndFoldersSelectionForm(Form):
|
||||
None,
|
||||
[
|
||||
# ('email', 'Email') if 'email' in available_template_types else None,
|
||||
(
|
||||
("sms", "Start with a blank template")
|
||||
if "sms" in available_template_types
|
||||
else None
|
||||
),
|
||||
(
|
||||
("copy-existing", "Copy an existing template")
|
||||
if allow_adding_copy_of_template
|
||||
else None
|
||||
),
|
||||
("sms", "Start with a blank template")
|
||||
if "sms" in available_template_types
|
||||
else None,
|
||||
("copy-existing", "Copy an existing template")
|
||||
if allow_adding_copy_of_template
|
||||
else None,
|
||||
],
|
||||
)
|
||||
)
|
||||
@@ -1888,6 +2064,13 @@ class AdminClearCacheForm(StripWhitespaceForm):
|
||||
raise ValidationError("Select at least one option")
|
||||
|
||||
|
||||
class AdminOrganizationGoLiveNotesForm(StripWhitespaceForm):
|
||||
request_to_go_live_notes = TextAreaField(
|
||||
"Go live notes",
|
||||
filters=[lambda x: x or None],
|
||||
)
|
||||
|
||||
|
||||
class ChangeSecurityKeyNameForm(StripWhitespaceForm):
|
||||
security_key_name = GovukTextInputField(
|
||||
"Name of key",
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from notifications_utils.field import Field
|
||||
from notifications_utils.formatters import formatted_list
|
||||
from notifications_utils.recipients import InvalidEmailError, validate_email_address
|
||||
from notifications_utils.sanitise_text import SanitiseSMS
|
||||
from wtforms import ValidationError
|
||||
|
||||
from app.main._commonly_used_passwords import commonly_used_passwords
|
||||
from app.models.spreadsheet import Spreadsheet
|
||||
from app.utils.user import is_gov_user
|
||||
from notifications_utils.field import Field
|
||||
from notifications_utils.formatters import formatted_list
|
||||
from notifications_utils.recipients import InvalidEmailError, validate_email_address
|
||||
from notifications_utils.sanitise_text import SanitiseSMS
|
||||
|
||||
|
||||
class CommonlyUsedPassword:
|
||||
@@ -161,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].*")
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
from flask import abort, render_template, request, url_for
|
||||
|
||||
from app import current_service, job_api_client
|
||||
from app.formatters import convert_time_unixtimestamp, 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
|
||||
|
||||
|
||||
@main.route("/activity/services/<uuid:service_id>")
|
||||
@user_has_permissions("view_activity")
|
||||
def all_jobs_activity(service_id):
|
||||
service_data_retention_days = 7
|
||||
page = get_page_from_request()
|
||||
jobs = job_api_client.get_page_of_jobs(service_id, page=page)
|
||||
all_jobs_dict = generate_job_dict(jobs)
|
||||
prev_page, next_page, pagination = handle_pagination(jobs, service_id, page)
|
||||
message_type = ("sms",)
|
||||
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,
|
||||
download_link_one_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="one_day",
|
||||
),
|
||||
download_link_three_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="three_day",
|
||||
),
|
||||
download_link_five_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="five_day",
|
||||
),
|
||||
download_link_seven_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="seven_day",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def handle_pagination(jobs, service_id, page):
|
||||
if page is None:
|
||||
abort(404, "Invalid page argument ({}).".format(request.args.get("page")))
|
||||
prev_page = (
|
||||
generate_previous_dict("main.all_jobs_activity", service_id, page)
|
||||
if page > 1
|
||||
else None
|
||||
)
|
||||
next_page = (
|
||||
generate_next_dict("main.all_jobs_activity", service_id, page)
|
||||
if jobs.get("links", {}).get("next")
|
||||
else None
|
||||
)
|
||||
pagination = generate_pagination_pages(
|
||||
jobs.get("total", {}), jobs.get("page_size", {}), page
|
||||
)
|
||||
return prev_page, next_page, pagination
|
||||
|
||||
|
||||
def generate_job_dict(jobs):
|
||||
return [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
"download_link": url_for(
|
||||
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"view_job_link": url_for(
|
||||
".view_job", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"created_at": job["created_at"],
|
||||
"time_sent_data_value": convert_time_unixtimestamp(
|
||||
job["processing_finished"]
|
||||
if job["processing_finished"]
|
||||
else (
|
||||
job["processing_started"]
|
||||
if job["processing_started"]
|
||||
else job["created_at"]
|
||||
)
|
||||
),
|
||||
"processing_finished": job["processing_finished"],
|
||||
"processing_started": job["processing_started"],
|
||||
"created_by": job["created_by"],
|
||||
"template_name": job["template_name"],
|
||||
"delivered_count": next(
|
||||
(
|
||||
stat["count"]
|
||||
for stat in job.get("statistics", [])
|
||||
if stat["status"] == "delivered"
|
||||
),
|
||||
None,
|
||||
),
|
||||
"failed_count": next(
|
||||
(
|
||||
stat["count"]
|
||||
for stat in job.get("statistics", [])
|
||||
if stat["status"] == "failed"
|
||||
),
|
||||
None,
|
||||
),
|
||||
}
|
||||
for job in jobs["data"]
|
||||
]
|
||||
@@ -41,8 +41,8 @@ def _create_example_template(service_id):
|
||||
|
||||
|
||||
@main.route("/add-service", methods=["GET", "POST"])
|
||||
@user_is_gov_user
|
||||
@user_is_logged_in
|
||||
@user_is_gov_user
|
||||
def add_service():
|
||||
default_organization_type = current_user.default_organization_type
|
||||
if default_organization_type is None:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from flask import abort, flash, redirect, render_template, request, url_for
|
||||
from flask import Markup, abort, flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
|
||||
from app import (
|
||||
api_key_api_client,
|
||||
@@ -169,14 +168,12 @@ def api_callbacks(service_id):
|
||||
|
||||
return render_template(
|
||||
"views/api/callbacks.html",
|
||||
received_text_messages_callback=(
|
||||
received_text_messages_callback["url"]
|
||||
if received_text_messages_callback
|
||||
else None
|
||||
),
|
||||
delivery_status_callback=(
|
||||
delivery_status_callback["url"] if delivery_status_callback else None
|
||||
),
|
||||
received_text_messages_callback=received_text_messages_callback["url"]
|
||||
if received_text_messages_callback
|
||||
else None,
|
||||
delivery_status_callback=delivery_status_callback["url"]
|
||||
if delivery_status_callback
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -265,11 +262,9 @@ def received_text_messages_callback(service_id):
|
||||
|
||||
received_text_messages_callback = get_received_text_messages_callback()
|
||||
form = CallbackForm(
|
||||
url=(
|
||||
received_text_messages_callback.get("url")
|
||||
if received_text_messages_callback
|
||||
else ""
|
||||
),
|
||||
url=received_text_messages_callback.get("url")
|
||||
if received_text_messages_callback
|
||||
else "",
|
||||
bearer_token=dummy_bearer_token if received_text_messages_callback else "",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from flask import jsonify, redirect, render_template, session, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
from notifications_utils.template import SMSPreviewTemplate
|
||||
|
||||
from app import current_service, notification_api_client, service_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import SearchByNameForm
|
||||
from app.models.template_list import TemplateList
|
||||
from app.utils.user import user_has_permissions
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
from notifications_utils.template import SMSPreviewTemplate
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/conversation/<uuid:notification_id>")
|
||||
|
||||
@@ -5,18 +5,17 @@ from itertools import groupby
|
||||
|
||||
from flask import Response, abort, jsonify, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from app import (
|
||||
billing_api_client,
|
||||
current_service,
|
||||
job_api_client,
|
||||
service_api_client,
|
||||
template_statistics_client,
|
||||
)
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric
|
||||
from app.main import main
|
||||
from app.main.views.user_profile import set_timezone
|
||||
from app.statistics_utils import get_formatted_percentage
|
||||
from app.utils import (
|
||||
DELIVERED_STATUSES,
|
||||
@@ -28,7 +27,6 @@ from app.utils.csv import Spreadsheet
|
||||
from app.utils.pagination import generate_next_dict, generate_previous_dict
|
||||
from app.utils.time import get_current_financial_year
|
||||
from app.utils.user import user_has_permissions
|
||||
from notifications_utils.recipients import format_phone_number_human_readable
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard")
|
||||
@@ -40,7 +38,6 @@ def old_service_dashboard(service_id):
|
||||
@main.route("/services/<uuid:service_id>")
|
||||
@user_has_permissions()
|
||||
def service_dashboard(service_id):
|
||||
|
||||
if session.get("invited_user_id"):
|
||||
session.pop("invited_user_id", None)
|
||||
session["service_id"] = service_id
|
||||
@@ -48,77 +45,13 @@ def service_dashboard(service_id):
|
||||
if not current_user.has_permissions("view_activity"):
|
||||
return redirect(url_for("main.choose_template", service_id=service_id))
|
||||
|
||||
yearly_usage = billing_api_client.get_annual_usage_for_service(
|
||||
service_id,
|
||||
get_current_financial_year(),
|
||||
)
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
current_service.id,
|
||||
)
|
||||
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
|
||||
sms_sent = usage_data["sms_sent"]
|
||||
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
|
||||
|
||||
job_response = job_api_client.get_jobs(service_id)["data"]
|
||||
service_data_retention_days = 7
|
||||
|
||||
jobs = [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
"download_link": url_for(
|
||||
".view_job_csv", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"view_job_link": url_for(
|
||||
".view_job", service_id=current_service.id, job_id=job["id"]
|
||||
),
|
||||
"created_at": job["created_at"],
|
||||
"processing_finished": job.get("processing_finished"),
|
||||
"processing_started": job.get("processing_started"),
|
||||
"notification_count": job["notification_count"],
|
||||
"created_by": job["created_by"],
|
||||
"template_name": job["template_name"],
|
||||
"original_file_name": job["original_file_name"],
|
||||
}
|
||||
for job in job_response
|
||||
if job["job_status"] != "cancelled"
|
||||
]
|
||||
return render_template(
|
||||
"views/dashboard/dashboard.html",
|
||||
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
|
||||
partials=get_dashboard_partials(service_id),
|
||||
jobs=jobs,
|
||||
service_data_retention_days=service_data_retention_days,
|
||||
sms_sent=sms_sent,
|
||||
sms_allowance_remaining=sms_allowance_remaining,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/daily_stats.json")
|
||||
def get_daily_stats():
|
||||
service_id = session.get("service_id")
|
||||
date_range = get_stats_date_range()
|
||||
|
||||
stats = service_api_client.get_service_notification_statistics_by_day(
|
||||
service_id, start_date=date_range["start_date"], days=date_range["days"]
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@main.route("/daily_stats_by_user.json")
|
||||
def get_daily_stats_by_user():
|
||||
service_id = session.get("service_id")
|
||||
date_range = get_stats_date_range()
|
||||
user_id = current_user.id
|
||||
stats = service_api_client.get_user_service_notification_statistics_by_day(
|
||||
service_id,
|
||||
user_id,
|
||||
start_date=date_range["start_date"],
|
||||
days=date_range["days"],
|
||||
)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard.json")
|
||||
@user_has_permissions("view_activity")
|
||||
def service_dashboard_updates(service_id):
|
||||
@@ -166,17 +99,11 @@ def template_usage(service_id):
|
||||
months=months,
|
||||
stats=stats,
|
||||
most_used_template_count=max(
|
||||
(
|
||||
max(
|
||||
(
|
||||
template["requested_count"]
|
||||
for template in month["templates_used"]
|
||||
),
|
||||
default=0,
|
||||
)
|
||||
for month in months
|
||||
),
|
||||
default=0,
|
||||
max(
|
||||
(template["requested_count"] for template in month["templates_used"]),
|
||||
default=0,
|
||||
)
|
||||
for month in months
|
||||
),
|
||||
years=get_tuples_of_financial_years(
|
||||
partial(url_for, ".template_usage", service_id=service_id),
|
||||
@@ -193,16 +120,31 @@ def usage(service_id):
|
||||
year, current_financial_year = requested_and_current_financial_year(request)
|
||||
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
service_id
|
||||
service_id, year
|
||||
)
|
||||
|
||||
units = billing_api_client.get_monthly_usage_for_service(service_id, year)
|
||||
|
||||
yearly_usage = billing_api_client.get_annual_usage_for_service(service_id, year)
|
||||
|
||||
more_stats = format_monthly_stats_to_list(
|
||||
service_api_client.get_monthly_notification_stats(service_id, year)["data"]
|
||||
)
|
||||
if year == current_financial_year:
|
||||
# This includes Oct, Nov, Dec
|
||||
# but we don't need next year's data yet
|
||||
more_stats = [
|
||||
month
|
||||
for month in more_stats
|
||||
if month["name"] in ["October", "November", "December"]
|
||||
]
|
||||
elif year == (current_financial_year + 1):
|
||||
# This is all the other months
|
||||
# and we need last year's data
|
||||
more_stats = [
|
||||
month
|
||||
for month in more_stats
|
||||
if month["name"] not in ["October", "November", "December"]
|
||||
]
|
||||
|
||||
return render_template(
|
||||
"views/usage.html",
|
||||
months=list(get_monthly_usage_breakdown(year, units, more_stats)),
|
||||
@@ -325,19 +267,13 @@ def aggregate_template_usage(template_statistics, sort_key="count"):
|
||||
key=lambda x: x["template_id"],
|
||||
):
|
||||
template_stats = list(v)
|
||||
first_stat = template_stats[0] if template_stats else None
|
||||
|
||||
templates.append(
|
||||
{
|
||||
"template_id": k,
|
||||
"template_name": first_stat.get("template_name"),
|
||||
"template_type": first_stat.get("template_type"),
|
||||
"template_name": template_stats[0]["template_name"],
|
||||
"template_type": template_stats[0]["template_type"],
|
||||
"count": sum(s["count"] for s in template_stats),
|
||||
"created_by": first_stat.get("created_by"),
|
||||
"created_by_id": first_stat.get("created_by_id"),
|
||||
"last_used": first_stat.get("last_used"),
|
||||
"status": first_stat.get("status"),
|
||||
"template_folder": first_stat.get("template_folder"),
|
||||
"template_folder_id": first_stat.get("template_folder_id"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -370,15 +306,8 @@ def get_dashboard_partials(service_id):
|
||||
dashboard_totals = (get_dashboard_totals(stats),)
|
||||
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
|
||||
current_service.id,
|
||||
get_current_financial_year(),
|
||||
)
|
||||
# These 2 calls will update the dashboard sms allowance count while in trial mode.
|
||||
billing_api_client.get_monthly_usage_for_service(
|
||||
service_id, get_current_financial_year()
|
||||
)
|
||||
billing_api_client.create_or_update_free_sms_fragment_limit(
|
||||
service_id, free_sms_fragment_limit=free_sms_allowance
|
||||
)
|
||||
|
||||
yearly_usage = billing_api_client.get_annual_usage_for_service(
|
||||
service_id,
|
||||
get_current_financial_year(),
|
||||
@@ -410,13 +339,11 @@ def get_dashboard_partials(service_id):
|
||||
|
||||
|
||||
def get_dashboard_totals(statistics):
|
||||
|
||||
for msg_type in statistics.values():
|
||||
msg_type["failed_percentage"] = get_formatted_percentage(
|
||||
msg_type["failed"], msg_type["requested"]
|
||||
)
|
||||
msg_type["show_warning"] = float(msg_type["failed_percentage"]) > 3
|
||||
|
||||
return statistics
|
||||
|
||||
|
||||
@@ -471,27 +398,13 @@ def aggregate_status_types(counts_dict):
|
||||
|
||||
|
||||
def get_months_for_financial_year(year, time_format="%B"):
|
||||
return [month.strftime(time_format) for month in (get_months_for_year(1, 13, year))]
|
||||
|
||||
|
||||
def get_current_month_for_financial_year(year):
|
||||
# Setting the timezone here because we need to set it somewhere.
|
||||
set_timezone()
|
||||
current_month = datetime.now().month
|
||||
return current_month
|
||||
|
||||
|
||||
def get_stats_date_range():
|
||||
current_financial_year = get_current_financial_year()
|
||||
current_month = get_current_month_for_financial_year(current_financial_year)
|
||||
start_date = datetime.now().strftime("%Y-%m-%d")
|
||||
days = 7
|
||||
return {
|
||||
"current_financial_year": current_financial_year,
|
||||
"current_month": current_month,
|
||||
"start_date": start_date,
|
||||
"days": days,
|
||||
}
|
||||
return [
|
||||
month.strftime(time_format)
|
||||
for month in (
|
||||
get_months_for_year(10, 13, year) + get_months_for_year(1, 10, year + 1)
|
||||
)
|
||||
if month < datetime.now()
|
||||
]
|
||||
|
||||
|
||||
def get_months_for_year(start, end, year):
|
||||
|
||||
133
app/main/views/email_branding.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from flask import current_app, redirect, render_template, session, url_for
|
||||
|
||||
from app import email_branding_client
|
||||
from app.main import main
|
||||
from app.main.forms import AdminEditEmailBrandingForm, SearchByNameForm
|
||||
from app.s3_client.s3_logo_client import (
|
||||
TEMP_TAG,
|
||||
delete_email_temp_file,
|
||||
delete_email_temp_files_created_by,
|
||||
permanent_email_logo_name,
|
||||
persist_logo,
|
||||
upload_email_logo,
|
||||
)
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
|
||||
@main.route("/email-branding", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def email_branding():
|
||||
brandings = email_branding_client.get_all_email_branding(sort_key="name")
|
||||
|
||||
return render_template(
|
||||
"views/email-branding/select-branding.html",
|
||||
email_brandings=brandings,
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit", methods=["GET", "POST"])
|
||||
@main.route("/email-branding/<uuid:branding_id>/edit/<logo>", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def update_email_branding(branding_id, logo=None):
|
||||
email_branding = email_branding_client.get_email_branding(branding_id)[
|
||||
"email_branding"
|
||||
]
|
||||
|
||||
form = AdminEditEmailBrandingForm(
|
||||
name=email_branding["name"],
|
||||
text=email_branding["text"],
|
||||
colour=email_branding["colour"],
|
||||
brand_type=email_branding["brand_type"],
|
||||
)
|
||||
|
||||
logo = logo if logo else email_branding.get("logo") if email_branding else None
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.file.data:
|
||||
upload_filename = upload_email_logo(
|
||||
form.file.data.filename, form.file.data, user_id=session["user_id"]
|
||||
)
|
||||
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
|
||||
delete_email_temp_file(logo)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".update_email_branding",
|
||||
branding_id=branding_id,
|
||||
logo=upload_filename,
|
||||
)
|
||||
)
|
||||
|
||||
updated_logo_name = (
|
||||
permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
)
|
||||
|
||||
email_branding_client.update_email_branding(
|
||||
branding_id=branding_id,
|
||||
logo=updated_logo_name,
|
||||
name=form.name.data,
|
||||
text=form.text.data,
|
||||
colour=form.colour.data,
|
||||
brand_type=form.brand_type.data,
|
||||
)
|
||||
|
||||
if logo:
|
||||
persist_logo(logo, updated_logo_name)
|
||||
|
||||
delete_email_temp_files_created_by(session["user_id"])
|
||||
|
||||
return redirect(url_for(".email_branding", branding_id=branding_id))
|
||||
|
||||
return render_template(
|
||||
"views/email-branding/manage-branding.html",
|
||||
form=form,
|
||||
email_branding=email_branding,
|
||||
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
|
||||
logo=logo,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/email-branding/create", methods=["GET", "POST"])
|
||||
@main.route("/email-branding/create/<logo>", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def create_email_branding(logo=None):
|
||||
form = AdminEditEmailBrandingForm(brand_type="org")
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.file.data:
|
||||
upload_filename = upload_email_logo(
|
||||
form.file.data.filename, form.file.data, user_id=session["user_id"]
|
||||
)
|
||||
|
||||
if logo and logo.startswith(TEMP_TAG.format(user_id=session["user_id"])):
|
||||
delete_email_temp_file(logo)
|
||||
|
||||
return redirect(url_for(".create_email_branding", logo=upload_filename))
|
||||
|
||||
updated_logo_name = (
|
||||
permanent_email_logo_name(logo, session["user_id"]) if logo else None
|
||||
)
|
||||
|
||||
email_branding_client.create_email_branding(
|
||||
logo=updated_logo_name,
|
||||
name=form.name.data,
|
||||
text=form.text.data,
|
||||
colour=form.colour.data,
|
||||
brand_type=form.brand_type.data,
|
||||
)
|
||||
|
||||
if logo:
|
||||
persist_logo(logo, updated_logo_name)
|
||||
|
||||
delete_email_temp_files_created_by(session["user_id"])
|
||||
|
||||
return redirect(url_for(".email_branding"))
|
||||
|
||||
return render_template(
|
||||
"views/email-branding/manage-branding.html",
|
||||
form=form,
|
||||
cdn_url=current_app.config["LOGO_CDN_DOMAIN"],
|
||||
logo=logo,
|
||||
)
|
||||
@@ -1,10 +1,239 @@
|
||||
from flask import render_template
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
from flask import redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from govuk_bank_holidays.bank_holidays import BankHolidays
|
||||
from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket
|
||||
|
||||
from app import convert_to_boolean, current_service
|
||||
from app.extensions import zendesk_client
|
||||
from app.main import main
|
||||
from app.utils.user import user_is_logged_in
|
||||
from app.main.forms import FeedbackOrProblem, SupportRedirect, SupportType, Triage
|
||||
from app.models.feedback import (
|
||||
GENERAL_TICKET_TYPE,
|
||||
PROBLEM_TICKET_TYPE,
|
||||
QUESTION_TICKET_TYPE,
|
||||
)
|
||||
from app.utils import hide_from_search_engines
|
||||
|
||||
bank_holidays = BankHolidays(use_cached_holidays=True)
|
||||
|
||||
|
||||
@main.route("/support", methods=["GET"])
|
||||
@user_is_logged_in
|
||||
@main.route("/support", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def support():
|
||||
return render_template("views/support/index.html")
|
||||
if current_user.is_authenticated:
|
||||
form = SupportType()
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(
|
||||
".feedback",
|
||||
ticket_type=form.support_type.data,
|
||||
)
|
||||
)
|
||||
else:
|
||||
form = SupportRedirect()
|
||||
if form.validate_on_submit():
|
||||
if form.who.data == "public":
|
||||
return redirect(url_for(".support_public"))
|
||||
else:
|
||||
return redirect(
|
||||
url_for(
|
||||
".feedback",
|
||||
ticket_type=GENERAL_TICKET_TYPE,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template("views/support/index.html", form=form)
|
||||
|
||||
|
||||
@main.route("/support/public")
|
||||
@hide_from_search_engines
|
||||
def support_public():
|
||||
return render_template("views/support/public.html")
|
||||
|
||||
|
||||
@main.route("/support/triage", methods=["GET", "POST"])
|
||||
@main.route("/support/triage/<ticket_type:ticket_type>", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def triage(ticket_type=PROBLEM_TICKET_TYPE):
|
||||
form = Triage()
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(".feedback", ticket_type=ticket_type, severe=form.severe.data)
|
||||
)
|
||||
return render_template(
|
||||
"views/support/triage.html",
|
||||
form=form,
|
||||
page_title={
|
||||
PROBLEM_TICKET_TYPE: "Report a problem",
|
||||
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
|
||||
}.get(ticket_type),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/support/<ticket_type:ticket_type>", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def feedback(ticket_type):
|
||||
form = FeedbackOrProblem()
|
||||
|
||||
if not form.feedback.data:
|
||||
form.feedback.data = session.pop("feedback_message", "")
|
||||
|
||||
if request.args.get("severe") in ["yes", "no"]:
|
||||
severe = convert_to_boolean(request.args.get("severe"))
|
||||
else:
|
||||
severe = None
|
||||
|
||||
out_of_hours_emergency = all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
not in_business_hours(),
|
||||
severe,
|
||||
)
|
||||
)
|
||||
|
||||
if needs_triage(ticket_type, severe):
|
||||
session["feedback_message"] = form.feedback.data
|
||||
return redirect(url_for(".triage", ticket_type=ticket_type))
|
||||
|
||||
if needs_escalation(ticket_type, severe):
|
||||
return redirect(url_for(".bat_phone"))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
form.email_address.data = current_user.email_address
|
||||
form.name.data = current_user.name
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_email = form.email_address.data
|
||||
user_name = form.name.data or None
|
||||
|
||||
feedback_msg = render_template(
|
||||
"support-tickets/support-ticket.txt",
|
||||
content=form.feedback.data,
|
||||
)
|
||||
|
||||
ticket = NotifySupportTicket(
|
||||
subject="Notify feedback",
|
||||
message=feedback_msg,
|
||||
ticket_type=get_zendesk_ticket_type(ticket_type),
|
||||
p1=out_of_hours_emergency,
|
||||
user_name=user_name,
|
||||
user_email=user_email,
|
||||
org_id=current_service.organization_id if current_service else None,
|
||||
org_type=current_service.organization_type if current_service else None,
|
||||
service_id=current_service.id if current_service else None,
|
||||
)
|
||||
zendesk_client.send_ticket_to_zendesk(ticket)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
".thanks",
|
||||
out_of_hours_emergency=out_of_hours_emergency,
|
||||
email_address_provided=(
|
||||
current_user.is_authenticated or bool(form.email_address.data)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/support/form.html",
|
||||
form=form,
|
||||
back_link=(
|
||||
url_for(".support")
|
||||
if severe is None
|
||||
else url_for(".triage", ticket_type=ticket_type)
|
||||
),
|
||||
show_status_page_banner=(ticket_type == PROBLEM_TICKET_TYPE),
|
||||
page_title={
|
||||
GENERAL_TICKET_TYPE: "Contact Notify.gov support",
|
||||
PROBLEM_TICKET_TYPE: "Report a problem",
|
||||
QUESTION_TICKET_TYPE: "Ask a question or give feedback",
|
||||
}.get(ticket_type),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/support/escalate", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def bat_phone():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("main.feedback", ticket_type=PROBLEM_TICKET_TYPE))
|
||||
|
||||
return render_template("views/support/bat-phone.html")
|
||||
|
||||
|
||||
@main.route("/support/thanks", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
def thanks():
|
||||
return render_template(
|
||||
"views/support/thanks.html",
|
||||
out_of_hours_emergency=convert_to_boolean(
|
||||
request.args.get("out_of_hours_emergency")
|
||||
),
|
||||
email_address_provided=convert_to_boolean(
|
||||
request.args.get("email_address_provided")
|
||||
),
|
||||
out_of_hours=not in_business_hours(),
|
||||
)
|
||||
|
||||
|
||||
def in_business_hours():
|
||||
now = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
|
||||
if is_weekend(now) or is_bank_holiday(now):
|
||||
return False
|
||||
|
||||
return london_time_today_as_utc(9, 30) <= now < london_time_today_as_utc(17, 30)
|
||||
|
||||
|
||||
def london_time_today_as_utc(hour, minute):
|
||||
return (
|
||||
pytz.timezone("Europe/London")
|
||||
.localize(datetime.now().replace(hour=hour, minute=minute))
|
||||
.astimezone(pytz.utc)
|
||||
)
|
||||
|
||||
|
||||
def is_weekend(time):
|
||||
return time.strftime("%A") in {
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
}
|
||||
|
||||
|
||||
def is_bank_holiday(time):
|
||||
return bank_holidays.is_holiday(time.date())
|
||||
|
||||
|
||||
def needs_triage(ticket_type, severe):
|
||||
return all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe is None,
|
||||
(not current_user.is_authenticated or current_user.live_services),
|
||||
not in_business_hours(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def needs_escalation(ticket_type, severe):
|
||||
return all(
|
||||
(
|
||||
ticket_type != QUESTION_TICKET_TYPE,
|
||||
severe,
|
||||
not current_user.is_authenticated,
|
||||
not in_business_hours(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_zendesk_ticket_type(ticket_type):
|
||||
# Zendesk has 4 ticket types - "problem", "incident", "task" and "question".
|
||||
# We don't want to use a Zendesk "problem" ticket type when someone reports a
|
||||
# Notify problem because they are designed to group multiple incident tickets together,
|
||||
# allowing them to be solved as a group.
|
||||
if ticket_type == PROBLEM_TICKET_TYPE:
|
||||
return NotifySupportTicket.TYPE_INCIDENT
|
||||
|
||||
return NotifySupportTicket.TYPE_QUESTION
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import os
|
||||
import secrets
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import abort, current_app, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import redis_client, status_api_client
|
||||
from app.formatters import apply_html_class, convert_markdown_template
|
||||
from app.main import main
|
||||
from app.main.views.pricing import CURRENT_SMS_RATE
|
||||
from app.main.views.sub_navigation_dictionaries import (
|
||||
best_practices_nav,
|
||||
features_nav,
|
||||
using_notify_nav,
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
make_response,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from notifications_utils.template import HTMLEmailTemplate
|
||||
|
||||
from app import email_branding_client, status_api_client
|
||||
from app.main import main
|
||||
from app.main.forms import FieldWithNoneOption
|
||||
from app.main.views.pricing import CURRENT_SMS_RATE
|
||||
from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav
|
||||
from app.utils.user import user_is_logged_in
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
|
||||
# Hook to check for guidance routes
|
||||
@main.before_request
|
||||
def check_guidance_feature():
|
||||
current_app.logger.warning("best practices 1234")
|
||||
current_app.logger.warning(current_app.config["FEATURE_BEST_PRACTICES_ENABLED"])
|
||||
if (
|
||||
request.path.startswith("/best-practices")
|
||||
and not current_app.config["FEATURE_BEST_PRACTICES_ENABLED"]
|
||||
):
|
||||
abort(404)
|
||||
|
||||
|
||||
@main.route("/")
|
||||
@@ -35,29 +23,10 @@ def index():
|
||||
if current_user and current_user.is_authenticated:
|
||||
return redirect(url_for("main.choose_account"))
|
||||
|
||||
# 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)
|
||||
|
||||
# make and store the nonce
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce)
|
||||
|
||||
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 render_template(
|
||||
"views/signedout.html",
|
||||
sms_rate=CURRENT_SMS_RATE,
|
||||
counts=status_api_client.get_count_of_live_services_and_organizations(),
|
||||
initial_signin_url=url,
|
||||
)
|
||||
|
||||
|
||||
@@ -94,6 +63,105 @@ def design_content():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/_email")
|
||||
@user_is_logged_in
|
||||
def email_template():
|
||||
branding_type = "govuk"
|
||||
branding_style = request.args.get("branding_style", None)
|
||||
|
||||
if branding_style == FieldWithNoneOption.NONE_OPTION_VALUE:
|
||||
branding_style = None
|
||||
|
||||
if branding_style is not None:
|
||||
email_branding = email_branding_client.get_email_branding(branding_style)[
|
||||
"email_branding"
|
||||
]
|
||||
branding_type = email_branding["brand_type"]
|
||||
|
||||
if branding_type == "govuk":
|
||||
brand_text = None
|
||||
brand_colour = None
|
||||
brand_logo = None
|
||||
govuk_banner = True
|
||||
brand_banner = False
|
||||
brand_name = None
|
||||
else:
|
||||
colour = email_branding["colour"]
|
||||
brand_text = email_branding["text"]
|
||||
brand_colour = colour
|
||||
brand_logo = (
|
||||
f"https://{current_app.config['LOGO_CDN_DOMAIN']}/{email_branding['logo']}"
|
||||
if email_branding["logo"]
|
||||
else None
|
||||
)
|
||||
govuk_banner = branding_type in ["govuk", "both"]
|
||||
brand_banner = branding_type == "org_banner"
|
||||
brand_name = email_branding["name"]
|
||||
|
||||
template = {
|
||||
"template_type": "email",
|
||||
"subject": "Email branding preview",
|
||||
"content": (
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting "
|
||||
"industry.\n\nLorem Ipsum has been the industry’s standard dummy "
|
||||
"text ever since the 1500s, when an unknown printer took a galley "
|
||||
"of type and scrambled it to make a type specimen book. "
|
||||
"\n\n"
|
||||
"# History"
|
||||
"\n\n"
|
||||
"It has "
|
||||
"survived not only"
|
||||
"\n\n"
|
||||
"* five centuries"
|
||||
"\n"
|
||||
"* but also the leap into electronic typesetting"
|
||||
"\n\n"
|
||||
"It was "
|
||||
"popularised in the 1960s with the release of Letraset sheets "
|
||||
"containing Lorem Ipsum passages, and more recently with desktop "
|
||||
"publishing software like Aldus PageMaker including versions of "
|
||||
"Lorem Ipsum."
|
||||
"\n\n"
|
||||
"^ It is a long established fact that a reader will be distracted "
|
||||
"by the readable content of a page when looking at its layout."
|
||||
"\n\n"
|
||||
"The point of using Lorem Ipsum is that it has a more-or-less "
|
||||
"normal distribution of letters, as opposed to using ‘Content "
|
||||
"here, content here’, making it look like readable English."
|
||||
"\n\n\n"
|
||||
"1. One"
|
||||
"\n"
|
||||
"2. Two"
|
||||
"\n"
|
||||
"10. Three"
|
||||
"\n\n"
|
||||
"This is an example of an email sent using Notify.gov."
|
||||
"\n\n"
|
||||
"https://www.notifications.service.gov.uk"
|
||||
),
|
||||
}
|
||||
|
||||
if not bool(request.args):
|
||||
resp = make_response(str(HTMLEmailTemplate(template)))
|
||||
else:
|
||||
resp = make_response(
|
||||
str(
|
||||
HTMLEmailTemplate(
|
||||
template,
|
||||
govuk_banner=govuk_banner,
|
||||
brand_text=brand_text,
|
||||
brand_colour=brand_colour,
|
||||
brand_logo=brand_logo,
|
||||
brand_banner=brand_banner,
|
||||
brand_name=brand_name,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
resp.headers["X-Frame-Options"] = "SAMEORIGIN"
|
||||
return resp
|
||||
|
||||
|
||||
@main.route("/documentation")
|
||||
@user_is_logged_in
|
||||
def documentation():
|
||||
@@ -128,6 +196,14 @@ def roadmap():
|
||||
return render_template("views/roadmap.html", navigation_links=features_nav())
|
||||
|
||||
|
||||
@main.route("/features/email")
|
||||
@user_is_logged_in
|
||||
def features_email():
|
||||
return render_template(
|
||||
"views/features/emails.html", navigation_links=features_nav()
|
||||
)
|
||||
|
||||
|
||||
@main.route("/features/sms")
|
||||
@user_is_logged_in
|
||||
def features_sms():
|
||||
@@ -142,7 +218,16 @@ def security():
|
||||
return render_template("views/security.html", navigation_links=features_nav())
|
||||
|
||||
|
||||
@main.route("/features/using_notify")
|
||||
@main.route("/features/terms", endpoint="terms")
|
||||
@user_is_logged_in
|
||||
def terms():
|
||||
return render_template(
|
||||
"views/terms-of-use.html",
|
||||
navigation_links=features_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/features/using-notify")
|
||||
@user_is_logged_in
|
||||
def using_notify():
|
||||
return (
|
||||
@@ -169,20 +254,9 @@ def get_started_old():
|
||||
@main.route("/using-notify/get-started")
|
||||
@user_is_logged_in
|
||||
def get_started():
|
||||
markdown = convert_markdown_template("get-started")
|
||||
html_style = apply_html_class(
|
||||
[
|
||||
["ol", "usa-process-list"],
|
||||
["li", "usa-process-list__item padding-bottom-4 margin-top-2"],
|
||||
["h2", "usa-process-list__heading line-height-sans-3"],
|
||||
],
|
||||
markdown,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/get-started.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
content=html_style,
|
||||
)
|
||||
|
||||
|
||||
@@ -205,78 +279,21 @@ def trial_mode_new():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices")
|
||||
@user_is_logged_in
|
||||
def best_practices():
|
||||
return render_template(
|
||||
"views/best-practices/best-practices.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/clear-goals")
|
||||
@user_is_logged_in
|
||||
def clear_goals():
|
||||
return render_template(
|
||||
"views/best-practices/clear-goals.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/rules-and-regulations")
|
||||
@user_is_logged_in
|
||||
def rules_and_regulations():
|
||||
return render_template(
|
||||
"views/best-practices/rules-and-regulations.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/establish-trust")
|
||||
@user_is_logged_in
|
||||
def establish_trust():
|
||||
return render_template(
|
||||
"views/best-practices/establish-trust.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/write-for-action")
|
||||
@user_is_logged_in
|
||||
def write_for_action():
|
||||
return render_template(
|
||||
"views/best-practices/write-for-action.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/multiple-languages")
|
||||
@user_is_logged_in
|
||||
def multiple_languages():
|
||||
return render_template(
|
||||
"views/best-practices/multiple-languages.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/best-practices/benchmark-performance")
|
||||
@user_is_logged_in
|
||||
def benchmark_performance():
|
||||
return render_template(
|
||||
"views/best-practices/benchmark-performance.html",
|
||||
navigation_links=best_practices_nav(),
|
||||
)
|
||||
|
||||
|
||||
@main.route("/using-notify/guidance")
|
||||
@user_is_logged_in
|
||||
def guidance_index():
|
||||
return render_template(
|
||||
"views/guidance/index.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
feature_best_practices_enabled=current_app.config[
|
||||
"FEATURE_BEST_PRACTICES_ENABLED"
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@main.route("/using-notify/guidance/branding-and-customisation")
|
||||
@user_is_logged_in
|
||||
def branding_and_customisation():
|
||||
return render_template(
|
||||
"views/guidance/branding-and-customisation.html",
|
||||
navigation_links=using_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
@@ -311,6 +328,7 @@ def send_files_by_email():
|
||||
|
||||
|
||||
@main.route("/roadmap", endpoint="old_roadmap")
|
||||
@main.route("/terms", endpoint="old_terms")
|
||||
@main.route("/information-security", endpoint="information_security")
|
||||
@main.route("/using_notify", endpoint="old_using_notify")
|
||||
@main.route("/information-risk-management", endpoint="information_risk_management")
|
||||
@@ -318,6 +336,7 @@ def send_files_by_email():
|
||||
def old_page_redirects():
|
||||
redirects = {
|
||||
"main.old_roadmap": "main.roadmap",
|
||||
"main.old_terms": "main.terms",
|
||||
"main.information_security": "main.using_notify",
|
||||
"main.old_using_notify": "main.using_notify",
|
||||
"main.information_risk_management": "main.security",
|
||||
|
||||
@@ -17,12 +17,14 @@ def accept_invite(token):
|
||||
and current_user.email_address.lower() != invited_user.email_address.lower()
|
||||
):
|
||||
message = Markup(
|
||||
f"""
|
||||
You’re signed in as {current_user.email_address}.
|
||||
This invite is for another email address.
|
||||
<a href={url_for("main.sign_out")} class="usa-link">Sign out</a>
|
||||
and click the link again to accept this invite.
|
||||
"""
|
||||
You’re signed in as {}.
|
||||
This invite is for another email address.
|
||||
<a href={} class="usa-link">Sign out</a>
|
||||
and click the link again to accept this invite.
|
||||
""".format(
|
||||
current_user.email_address, url_for("main.sign_out")
|
||||
)
|
||||
)
|
||||
|
||||
flash(message=message)
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
from functools import partial
|
||||
|
||||
from flask import (
|
||||
Markup,
|
||||
Response,
|
||||
abort,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
stream_with_context,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTemplate
|
||||
|
||||
from app import (
|
||||
current_service,
|
||||
format_datetime_table,
|
||||
format_datetime_short,
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
)
|
||||
@@ -34,7 +34,6 @@ from app.utils.pagination import (
|
||||
get_page_from_request,
|
||||
)
|
||||
from app.utils.user import user_has_permissions
|
||||
from notifications_utils.template import EmailPreviewTemplate, SMSBodyPreviewTemplate
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/jobs")
|
||||
@@ -94,7 +93,7 @@ def view_job_csv(service_id, job_id):
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": 'inline; filename="{} - {}.csv"'.format(
|
||||
job.template["name"], format_datetime_table(job.created_at)
|
||||
job.template["name"], format_datetime_short(job.created_at)
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -143,40 +142,11 @@ def view_notifications(service_id, message_type=None):
|
||||
True: ["reference"],
|
||||
False: [],
|
||||
}.get(bool(current_service.api_keys)),
|
||||
download_link_one_day=url_for(
|
||||
download_link=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="one_day",
|
||||
),
|
||||
download_link_today=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="today",
|
||||
),
|
||||
download_link_three_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="three_day",
|
||||
),
|
||||
download_link_five_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="five_day",
|
||||
),
|
||||
download_link_seven_day=url_for(
|
||||
".download_notifications_csv",
|
||||
service_id=current_service.id,
|
||||
message_type=message_type,
|
||||
status=request.args.get("status"),
|
||||
number_of_days="seven_day",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -212,9 +182,10 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
service_data_retention_days = None
|
||||
search_term = request.form.get("to", "")
|
||||
|
||||
if message_type is not None:
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
message_type, number_of_days="seven_day"
|
||||
message_type
|
||||
)
|
||||
|
||||
if request.path.endswith("csv") and current_user.has_permissions("view_activity"):
|
||||
@@ -240,6 +211,7 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
|
||||
)
|
||||
url_args = {"message_type": message_type, "status": request.args.get("status")}
|
||||
prev_page = None
|
||||
|
||||
if "links" in notifications and notifications["links"].get("prev", None):
|
||||
prev_page = generate_previous_dict(
|
||||
"main.view_notifications", service_id, page, url_args=url_args
|
||||
@@ -260,6 +232,7 @@ def get_notifications(service_id, message_type, status_override=None): # noqa
|
||||
)
|
||||
else:
|
||||
download_link = None
|
||||
|
||||
return {
|
||||
"service_data_retention_days": service_data_retention_days,
|
||||
"counts": render_template(
|
||||
@@ -303,16 +276,12 @@ def get_status_filters(service, message_type, statistics):
|
||||
}
|
||||
else:
|
||||
stats = statistics[message_type]
|
||||
|
||||
if stats.get("failure") is not None:
|
||||
stats["failed"] = stats["failure"]
|
||||
|
||||
stats["pending"] = stats["requested"] - stats["delivered"] - stats["failed"]
|
||||
stats["sending"] = stats["requested"] - stats["delivered"] - stats["failed"]
|
||||
|
||||
filters = [
|
||||
# key, label, option
|
||||
("requested", "total", "sending,delivered,failed"),
|
||||
("pending", "pending", "sending,pending"),
|
||||
("sending", "pending", "pending"),
|
||||
("delivered", "delivered", "delivered"),
|
||||
("failed", "failed", "failed"),
|
||||
]
|
||||
@@ -327,7 +296,7 @@ def get_status_filters(service, message_type, statistics):
|
||||
message_type=message_type,
|
||||
status=option,
|
||||
),
|
||||
stats.get(key),
|
||||
stats[key],
|
||||
)
|
||||
for key, label, option in filters
|
||||
]
|
||||
@@ -388,7 +357,6 @@ def get_job_partials(job):
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
notifications = job.get_notifications(status=filter_args["status"])
|
||||
number_of_days = "seven_day"
|
||||
counts = render_template(
|
||||
"partials/count.html",
|
||||
counts=_get_job_counts(job),
|
||||
@@ -398,16 +366,9 @@ def get_job_partials(job):
|
||||
),
|
||||
)
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
job.template_type, number_of_days
|
||||
job.template_type
|
||||
)
|
||||
|
||||
if request.referrer is not None:
|
||||
session["arrived_from_preview_page"] = "check" in request.referrer
|
||||
else:
|
||||
session["arrived_from_preview_page"] = False
|
||||
|
||||
arrived_from_preview_page_url = session.get("arrived_from_preview_page", False)
|
||||
|
||||
return {
|
||||
"counts": counts,
|
||||
"notifications": render_template(
|
||||
@@ -431,7 +392,6 @@ def get_job_partials(job):
|
||||
"status": render_template(
|
||||
"partials/jobs/status.html",
|
||||
job=job,
|
||||
arrived_from_preview_page_url=arrived_from_preview_page_url,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from app.event_handlers import (
|
||||
create_invite_user_to_service_event,
|
||||
create_mobile_number_change_event,
|
||||
create_remove_user_from_service_event,
|
||||
create_resend_user_invite_to_service_event,
|
||||
)
|
||||
from app.formatters import redact_mobile_number
|
||||
from app.main import main
|
||||
@@ -46,6 +45,7 @@ def manage_users(service_id):
|
||||
@user_has_permissions("manage_service")
|
||||
def invite_user(service_id, user_id=None):
|
||||
form_class = InviteUserForm
|
||||
|
||||
form = form_class(
|
||||
inviter_email_address=current_user.email_address,
|
||||
all_template_folders=current_service.all_template_folders,
|
||||
@@ -77,11 +77,6 @@ def invite_user(service_id, user_id=None):
|
||||
form.login_authentication.data = "sms_auth"
|
||||
|
||||
if form.validate_on_submit():
|
||||
if not form.permissions or len(form.permissions) == 0:
|
||||
return render_template(
|
||||
"views/user-has-no-permissions.html",
|
||||
user_to_invite=user_to_invite,
|
||||
)
|
||||
email_address = form.email_address.data
|
||||
invited_user = InvitedUser.create(
|
||||
current_user.id,
|
||||
@@ -127,18 +122,16 @@ def edit_user_permissions(service_id, user_id):
|
||||
form = form_class.from_user(
|
||||
user,
|
||||
service_id,
|
||||
folder_permissions=(
|
||||
None
|
||||
if user.platform_admin
|
||||
else [
|
||||
f["id"]
|
||||
for f in current_service.all_template_folders
|
||||
if user.has_template_folder_permission(f)
|
||||
]
|
||||
),
|
||||
all_template_folders=(
|
||||
None if user.platform_admin else current_service.all_template_folders
|
||||
),
|
||||
folder_permissions=None
|
||||
if user.platform_admin
|
||||
else [
|
||||
f["id"]
|
||||
for f in current_service.all_template_folders
|
||||
if user.has_template_folder_permission(f)
|
||||
],
|
||||
all_template_folders=None
|
||||
if user.platform_admin
|
||||
else current_service.all_template_folders,
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
@@ -339,22 +332,3 @@ def cancel_invited_user(service_id, invited_user_id):
|
||||
|
||||
flash(f"Invitation cancelled for {invited_user.email_address}", "default_with_tick")
|
||||
return redirect(url_for("main.manage_users", service_id=service_id))
|
||||
|
||||
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/resend-invite/<uuid:invited_user_id>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
def resend_invite(service_id, invited_user_id):
|
||||
current_service.resend_invite(invited_user_id)
|
||||
|
||||
invited_user = InvitedUser.by_id_and_service_id(service_id, invited_user_id)
|
||||
create_resend_user_invite_to_service_event(
|
||||
email_address=invited_user.email_address,
|
||||
resent_by_id=current_user.id,
|
||||
service_id=service_id,
|
||||
)
|
||||
|
||||
flash(f"Invitation resent for {invited_user.email_address}", "default_with_tick")
|
||||
return redirect(url_for("main.manage_users", service_id=service_id))
|
||||
|
||||
@@ -10,12 +10,12 @@ from flask import (
|
||||
url_for,
|
||||
)
|
||||
from itsdangerous import SignatureExpired
|
||||
from notifications_utils.url_safe_token import check_token
|
||||
|
||||
from app.main import main
|
||||
from app.main.forms import NewPasswordForm
|
||||
from app.models.user import User
|
||||
from app.utils.login import log_in_user
|
||||
from notifications_utils.url_safe_token import check_token
|
||||
|
||||
|
||||
@main.route("/new-password/<path:token>", methods=["GET", "POST"])
|
||||
|
||||
@@ -3,19 +3,21 @@ from datetime import datetime
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
flash,
|
||||
jsonify,
|
||||
render_template,
|
||||
request,
|
||||
stream_with_context,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import current_user
|
||||
|
||||
from app import current_service, job_api_client, notification_api_client
|
||||
from app import (
|
||||
current_service,
|
||||
format_date_numeric,
|
||||
job_api_client,
|
||||
notification_api_client,
|
||||
)
|
||||
from app.main import main
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_TEST
|
||||
from app.s3_client.s3_csv_client import delete_report, report_upload
|
||||
from app.utils import (
|
||||
DELIVERED_STATUSES,
|
||||
FAILURE_STATUSES,
|
||||
@@ -23,23 +25,21 @@ from app.utils import (
|
||||
parse_filter_args,
|
||||
set_status_filters,
|
||||
)
|
||||
from app.utils.csv import generate_notifications_csv, get_user_preferred_timezone
|
||||
from app.utils.csv import generate_notifications_csv
|
||||
from app.utils.templates import get_template
|
||||
from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
def view_notification(service_id, notification_id, error_message=None):
|
||||
if error_message:
|
||||
flash(error_message)
|
||||
|
||||
def view_notification(service_id, notification_id):
|
||||
notification = notification_api_client.get_notification(
|
||||
service_id, str(notification_id)
|
||||
)
|
||||
notification["template"].update({"reply_to_text": notification["reply_to_text"]})
|
||||
|
||||
personalisation = get_all_personalisation_from_notification(notification)
|
||||
error_message = None
|
||||
|
||||
template = get_template(
|
||||
notification["template"],
|
||||
@@ -92,7 +92,7 @@ def view_notification(service_id, notification_id, error_message=None):
|
||||
partials=get_single_notification_partials(notification),
|
||||
created_by=notification.get("created_by"),
|
||||
created_at=notification["created_at"],
|
||||
updated_at=notification["sent_at"],
|
||||
updated_at=notification["updated_at"],
|
||||
help=get_help_argument(),
|
||||
notification_id=notification["id"],
|
||||
can_receive_inbound=(current_service.has_permission("inbound_sms")),
|
||||
@@ -139,43 +139,10 @@ def get_all_personalisation_from_notification(notification):
|
||||
def download_notifications_csv(service_id):
|
||||
filter_args = parse_filter_args(request.args)
|
||||
filter_args["status"] = set_status_filters(filter_args)
|
||||
number_of_days = request.args["number_of_days"]
|
||||
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
filter_args.get("message_type")[0], number_of_days
|
||||
filter_args.get("message_type")[0]
|
||||
)
|
||||
file_time = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
|
||||
file_time = f"{file_time} {get_user_preferred_timezone()}"
|
||||
|
||||
csv = generate_notifications_csv(
|
||||
service_id=service_id,
|
||||
job_id=None,
|
||||
status=filter_args.get("status"),
|
||||
page=request.args.get("page", 1),
|
||||
page_size=10000,
|
||||
format_for_csv=True,
|
||||
template_type=filter_args.get("message_type"),
|
||||
limit_days=service_data_retention_days,
|
||||
)
|
||||
|
||||
# START asynchronous reporting block
|
||||
csv_file = "".join(csv)
|
||||
file_location = f"reports/{service_id}/{current_user.id}/{service_data_retention_days}/report.csv"
|
||||
|
||||
# TODO these are some capabilities we will probably need when
|
||||
# report generation becomes asynchronous.
|
||||
|
||||
# old_content = report_download(file_location)
|
||||
# current_app.logger.info(f"OLD CONTENT IS {old_content}")
|
||||
# reports = get_downloadable_reports(current_user.id, service_id)
|
||||
|
||||
# TODO these are to support asynchronous report generation.
|
||||
# Leaving them commented in so they get exercised.
|
||||
delete_report(file_location)
|
||||
report_upload(file_location, csv_file)
|
||||
# END asynchronous reporting block
|
||||
|
||||
# TODO eventually we want to remove this, when reports become fully asynchronous
|
||||
# The UI should be retrieving the report elsewhere via the download_report() method call
|
||||
return Response(
|
||||
stream_with_context(
|
||||
generate_notifications_csv(
|
||||
@@ -192,7 +159,7 @@ def download_notifications_csv(service_id):
|
||||
mimetype="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": 'inline; filename="{} - {} - {} report.csv"'.format(
|
||||
file_time,
|
||||
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
|
||||
filter_args["message_type"][0],
|
||||
current_service.name,
|
||||
)
|
||||
|
||||
@@ -6,13 +6,21 @@ from flask import flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import current_organization, org_invite_api_client, organizations_client
|
||||
from app import (
|
||||
current_organization,
|
||||
email_branding_client,
|
||||
org_invite_api_client,
|
||||
organizations_client,
|
||||
)
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
AdminBillingDetailsForm,
|
||||
AdminNewOrganizationForm,
|
||||
AdminNotesForm,
|
||||
AdminOrganizationDomainsForm,
|
||||
AdminOrganizationGoLiveNotesForm,
|
||||
AdminPreviewBrandingForm,
|
||||
AdminSetEmailBrandingForm,
|
||||
InviteOrgUserForm,
|
||||
OrganizationOrganizationTypeForm,
|
||||
RenameOrganizationForm,
|
||||
@@ -23,6 +31,7 @@ from app.main.views.dashboard import (
|
||||
get_tuples_of_financial_years,
|
||||
requested_and_current_financial_year,
|
||||
)
|
||||
from app.main.views.service_settings import get_branding_as_value_and_label
|
||||
from app.models.organization import AllOrganizations, Organization
|
||||
from app.models.user import InvitedOrgUser, User
|
||||
from app.utils.csv import Spreadsheet
|
||||
@@ -274,6 +283,58 @@ def edit_organization_type(org_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/set-email-branding", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_email_branding(org_id):
|
||||
email_branding = email_branding_client.get_all_email_branding()
|
||||
|
||||
form = AdminSetEmailBrandingForm(
|
||||
all_branding_options=get_branding_as_value_and_label(email_branding),
|
||||
current_branding=current_organization.email_branding_id,
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for(
|
||||
".organization_preview_email_branding",
|
||||
org_id=org_id,
|
||||
branding_style=form.branding_style.data,
|
||||
)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/organizations/organization/settings/set-email-branding.html",
|
||||
form=form,
|
||||
search_form=SearchByNameForm(),
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/preview-email-branding",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def organization_preview_email_branding(org_id):
|
||||
branding_style = request.args.get("branding_style", None)
|
||||
|
||||
form = AdminPreviewBrandingForm(branding_style=branding_style)
|
||||
|
||||
if form.validate_on_submit():
|
||||
current_organization.update(
|
||||
email_branding_id=form.branding_style.data,
|
||||
delete_services_cache=True,
|
||||
)
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
return render_template(
|
||||
"views/organizations/organization/settings/preview-email-branding.html",
|
||||
form=form,
|
||||
action=url_for("main.organization_preview_email_branding", org_id=org_id),
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/edit-organization-domains",
|
||||
methods=["GET", "POST"],
|
||||
@@ -312,6 +373,28 @@ def edit_organization_domains(org_id):
|
||||
)
|
||||
|
||||
|
||||
@main.route(
|
||||
"/organizations/<uuid:org_id>/settings/edit-go-live-notes", methods=["GET", "POST"]
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def edit_organization_go_live_notes(org_id):
|
||||
form = AdminOrganizationGoLiveNotesForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
organizations_client.update_organization(
|
||||
org_id, request_to_go_live_notes=form.request_to_go_live_notes.data
|
||||
)
|
||||
return redirect(url_for(".organization_settings", org_id=org_id))
|
||||
|
||||
org = organizations_client.get_organization(org_id)
|
||||
form.request_to_go_live_notes.data = org["request_to_go_live_notes"]
|
||||
|
||||
return render_template(
|
||||
"views/organizations/organization/settings/edit-go-live-notes.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/organizations/<uuid:org_id>/settings/notes", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_organization_notes(org_id):
|
||||
|
||||
@@ -8,15 +8,13 @@ from flask import render_template
|
||||
|
||||
from app import performance_dashboard_api_client, status_api_client
|
||||
from app.main import main
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
|
||||
|
||||
@main.route("/performance")
|
||||
def performance():
|
||||
preferred_tz = pytz.timezone(get_user_preferred_timezone())
|
||||
stats = performance_dashboard_api_client.get_performance_dashboard_stats(
|
||||
start_date=(datetime.now(preferred_tz) - timedelta(days=7)).date(),
|
||||
end_date=datetime.now(preferred_tz).date(),
|
||||
start_date=(datetime.now(pytz.utc) - timedelta(days=7)).date(),
|
||||
end_date=datetime.now(pytz.utc).date(),
|
||||
)
|
||||
stats["organizations_using_notify"] = sorted(
|
||||
[
|
||||
@@ -34,8 +32,7 @@ def performance():
|
||||
stats["average_percentage_under_10_seconds"] = mean(
|
||||
[row["percentage_under_10_seconds"] for row in stats["processing_time"]] or [0]
|
||||
)
|
||||
stats["count_of_live_services_and_organizations"] = (
|
||||
status_api_client.get_count_of_live_services_and_organizations()
|
||||
)
|
||||
|
||||
stats[
|
||||
"count_of_live_services_and_organizations"
|
||||
] = status_api_client.get_count_of_live_services_and_organizations()
|
||||
return render_template("views/performance.html", **stats)
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import csv
|
||||
import itertools
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
from flask import (
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask import abort, flash, render_template, request, url_for
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app import (
|
||||
@@ -34,7 +23,6 @@ from app.main.forms import (
|
||||
DateFilterForm,
|
||||
RequiredDateFilterForm,
|
||||
)
|
||||
from app.main.views.send import _send_notification
|
||||
from app.statistics_utils import (
|
||||
get_formatted_percentage,
|
||||
get_formatted_percentage_two_dp,
|
||||
@@ -82,40 +70,6 @@ def platform_admin():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/platform-admin/download-all-users")
|
||||
@user_is_platform_admin
|
||||
def download_all_users():
|
||||
|
||||
# Create a CSV string from the user data
|
||||
users = user_api_client.get_all_users_detailed()
|
||||
|
||||
if len(users) == 0:
|
||||
return "No data to download."
|
||||
|
||||
output = StringIO()
|
||||
header = ["Name", "Email Address", "Phone Number", "Service"]
|
||||
fieldnames = ["name", "email_address", "mobile_number", "service"]
|
||||
writer = csv.DictWriter(
|
||||
output,
|
||||
fieldnames=fieldnames,
|
||||
delimiter=",",
|
||||
)
|
||||
# Write custom header
|
||||
writer.writerow(dict(zip(fieldnames, header)))
|
||||
for user in users:
|
||||
user_no_commas = {key: value.replace(",", "") for key, value in user.items()}
|
||||
if user_no_commas["name"].startswith("e2e"):
|
||||
continue
|
||||
writer.writerow(user_no_commas)
|
||||
csv_data = output.getvalue()
|
||||
|
||||
# Create a direct download response with the CSV data and appropriate headers
|
||||
response = Response(csv_data, content_type="text/csv; charset=utf-8")
|
||||
response.headers["Content-Disposition"] = "attachment; filename=users.csv"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def is_over_threshold(number, total, threshold):
|
||||
percentage = number / total * 100 if total else 0
|
||||
return percentage > threshold
|
||||
@@ -642,6 +596,13 @@ def clear_cache():
|
||||
"service-????????-????-????-????-????????????-template-????????-????-????-????-????????????-versions", # noqa
|
||||
],
|
||||
),
|
||||
(
|
||||
"email_branding",
|
||||
[
|
||||
"email_branding",
|
||||
"email_branding-????????-????-????-????-????????????",
|
||||
],
|
||||
),
|
||||
(
|
||||
"organization",
|
||||
[
|
||||
@@ -708,21 +669,7 @@ def create_global_stats(services):
|
||||
"email": {"delivered": 0, "failed": 0, "requested": 0},
|
||||
"sms": {"delivered": 0, "failed": 0, "requested": 0},
|
||||
}
|
||||
# Issue #1323. The back end is now sending 'failure' instead of
|
||||
# 'failed'. Adjust it here, but keep it flexible in case
|
||||
# the backend reverts to 'failed'.
|
||||
for service in services:
|
||||
if service["statistics"]["sms"].get("failure") is not None:
|
||||
service["statistics"]["sms"]["failed"] = service["statistics"]["sms"][
|
||||
"failure"
|
||||
]
|
||||
if service["statistics"]["email"].get("failure") is not None:
|
||||
service["statistics"]["email"]["failed"] = service["statistics"]["email"][
|
||||
"failure"
|
||||
]
|
||||
|
||||
for service in services:
|
||||
|
||||
for msg_type, status in itertools.product(
|
||||
("sms", "email"), ("delivered", "failed", "requested")
|
||||
):
|
||||
@@ -742,6 +689,7 @@ def format_stats_by_service(services):
|
||||
"name": service["name"],
|
||||
"stats": service["statistics"],
|
||||
"restricted": service["restricted"],
|
||||
"research_mode": service["research_mode"],
|
||||
"created_at": service["created_at"],
|
||||
"active": service["active"],
|
||||
}
|
||||
@@ -781,80 +729,3 @@ def _get_user_row(r):
|
||||
row.append(r["password_changed_at"])
|
||||
row.append(r["state"])
|
||||
return row
|
||||
|
||||
|
||||
@main.route(
|
||||
"/platform-admin/load-test",
|
||||
methods=["POST", "GET"],
|
||||
)
|
||||
@user_is_platform_admin
|
||||
def load_test():
|
||||
"""
|
||||
The load test assumes that a service called 'Test service' exists. It will make
|
||||
the platform admin a member of this service if the platform is not already. All
|
||||
messagese will be sent in this service.
|
||||
"""
|
||||
service = _find_load_test_service()
|
||||
_prepare_load_test_service(service)
|
||||
example_template = _find_example_template(service)
|
||||
|
||||
# Simulated success
|
||||
for _ in range(0, 250):
|
||||
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][0]
|
||||
session["placeholders"] = {
|
||||
"day of week": "Monday",
|
||||
"color": "blue",
|
||||
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][0],
|
||||
}
|
||||
_send_notification(service["id"], example_template["id"])
|
||||
# Simulated failure
|
||||
for _ in range(0, 250):
|
||||
session["recipient"] = current_app.config["SIMULATED_SMS_NUMBERS"][1]
|
||||
session["placeholders"] = {
|
||||
"day of week": "Wednesday",
|
||||
"color": "orange",
|
||||
"phone number": current_app.config["SIMULATED_SMS_NUMBERS"][1],
|
||||
}
|
||||
_send_notification(service["id"], example_template["id"])
|
||||
|
||||
# For now, just redirect to the splash page so we know it's done
|
||||
return render_template(
|
||||
"views/platform-admin/splash-page.html",
|
||||
)
|
||||
|
||||
|
||||
def _find_example_template(service):
|
||||
templates = service_api_client.get_service_templates(service["id"])
|
||||
templates = templates["data"]
|
||||
for template in templates:
|
||||
# template = json.loads(template)
|
||||
if template["name"] == "Example text message template":
|
||||
return template
|
||||
|
||||
raise Exception("Could not find example template for load test")
|
||||
|
||||
|
||||
def _find_load_test_service():
|
||||
services = service_api_client.find_services_by_name("Test service")
|
||||
services = services["data"]
|
||||
|
||||
for service in services:
|
||||
if service["name"] == "Test service":
|
||||
return service
|
||||
|
||||
raise Exception("Could not find 'Test service' for load test")
|
||||
|
||||
|
||||
def _prepare_load_test_service(service):
|
||||
users = user_api_client.get_all_users()
|
||||
for user in users:
|
||||
if user["platform_admin"] == "t":
|
||||
try:
|
||||
user_api_client.add_user_to_service(
|
||||
service["id"], user["id"], ["send messages"]
|
||||
)
|
||||
except Exception:
|
||||
current_app.logger.exception(
|
||||
"Couldnt add user, may already be part of service"
|
||||
)
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from flask import current_app, render_template
|
||||
from flask_login import current_user
|
||||
from notifications_utils.international_billing_rates import INTERNATIONAL_BILLING_RATES
|
||||
|
||||
from app.main import main
|
||||
from app.main.forms import SearchByNameForm
|
||||
from app.main.views.sub_navigation_dictionaries import using_notify_nav
|
||||
from app.utils.user import user_is_logged_in
|
||||
from notifications_utils.international_billing_rates import INTERNATIONAL_BILLING_RATES
|
||||
|
||||
CURRENT_SMS_RATE = "1.72"
|
||||
|
||||
|
||||
79
app/main/views/providers.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from datetime import datetime
|
||||
|
||||
from flask import render_template, url_for
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
from app import provider_client
|
||||
from app.main import main
|
||||
from app.main.forms import AdminProviderRatioForm
|
||||
from app.utils.user import user_is_platform_admin
|
||||
|
||||
PROVIDER_PRIORITY_MEANING_SWITCHOVER = datetime(2019, 11, 29, 11, 0).isoformat()
|
||||
|
||||
|
||||
@main.route("/providers")
|
||||
@user_is_platform_admin
|
||||
def view_providers():
|
||||
providers = provider_client.get_all_providers()["provider_details"]
|
||||
domestic_email_providers, domestic_sms_providers, intl_sms_providers = [], [], []
|
||||
for provider in providers:
|
||||
if provider["notification_type"] == "sms":
|
||||
domestic_sms_providers.append(provider)
|
||||
if provider.get("supports_international", None):
|
||||
intl_sms_providers.append(provider)
|
||||
elif provider["notification_type"] == "email":
|
||||
domestic_email_providers.append(provider)
|
||||
|
||||
add_monthly_traffic(domestic_sms_providers)
|
||||
|
||||
return render_template(
|
||||
"views/providers/providers.html",
|
||||
email_providers=domestic_email_providers,
|
||||
domestic_sms_providers=domestic_sms_providers,
|
||||
intl_sms_providers=intl_sms_providers,
|
||||
)
|
||||
|
||||
|
||||
def add_monthly_traffic(domestic_sms_providers):
|
||||
total_sms_sent = sum(
|
||||
provider["current_month_billable_sms"] for provider in domestic_sms_providers
|
||||
)
|
||||
|
||||
for provider in domestic_sms_providers:
|
||||
percentage = (
|
||||
(provider["current_month_billable_sms"] / total_sms_sent * 100)
|
||||
if total_sms_sent
|
||||
else 0
|
||||
)
|
||||
provider["monthly_traffic"] = round(percentage)
|
||||
|
||||
|
||||
@main.route("/provider/edit-sms-provider-ratio", methods=["GET", "POST"])
|
||||
@user_is_platform_admin
|
||||
def edit_sms_provider_ratio():
|
||||
providers = [
|
||||
provider
|
||||
for provider in provider_client.get_all_providers()["provider_details"]
|
||||
if provider["notification_type"] == "sms" and provider["active"]
|
||||
]
|
||||
|
||||
form = AdminProviderRatioForm(providers)
|
||||
|
||||
if form.validate_on_submit():
|
||||
for provider in providers:
|
||||
field = getattr(form, provider["identifier"])
|
||||
provider_client.update_provider(provider["id"], field.data)
|
||||
return redirect(url_for(".view_providers"))
|
||||
|
||||
return render_template(
|
||||
"views/providers/edit-sms-provider-ratio.html", form=form, providers=providers
|
||||
)
|
||||
|
||||
|
||||
@main.route("/provider/<uuid:provider_id>")
|
||||
@user_is_platform_admin
|
||||
def view_provider(provider_id):
|
||||
versions = provider_client.get_provider_versions(provider_id)
|
||||
return render_template(
|
||||
"views/providers/provider.html", provider_versions=versions["data"]
|
||||
)
|
||||