Compare commits
4 Commits
11-20-2024
...
create-sta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d94e45da3 | ||
|
|
3a6e5e9c2d | ||
|
|
ed827d0c29 | ||
|
|
013154d5cf |
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": 125,
|
||||
"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-11-14T15:53:44Z"
|
||||
}
|
||||
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 }}
|
||||
|
||||
4
.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
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.3"
|
||||
python-version: "3.12"
|
||||
- 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'
|
||||
|
||||
|
||||
77
.github/pull_request_template.md
vendored
@@ -1,22 +1,79 @@
|
||||
<!--
|
||||
Not sure what you should include or write in a pull request? Please read the
|
||||
[pull request documentation in our docs!](https://github.com/GSA/notifications-api/blob/main/docs/all.md#pull-requests)
|
||||
Please follow the instructions found in this pull request template so that we
|
||||
have all of the relevant details needed for our work.
|
||||
|
||||
At the minimum, please be sure to fill in all sections found below and also do
|
||||
the following:
|
||||
|
||||
- Provide an appropriate and descriptive title for the pull request
|
||||
- Link the pull request to its corresponding issue (must be done after creating
|
||||
the pull request itself)
|
||||
- Assign yourself as the author
|
||||
- Attach the appropriate labels to it
|
||||
- Set it to be on the Notify.gov project board
|
||||
- Select one or more reviewers from the team or mark the pull request as a draft
|
||||
depending on its current state
|
||||
- If the pull request is a draft, please be sure to add reviewers once it is
|
||||
ready for review and mark it ready for review
|
||||
|
||||
For each section, please delete the instructions/sample text (that includes this
|
||||
text, though it is wrapped in an HTML comment just in case) and put in your own
|
||||
information. Thank you!
|
||||
-->
|
||||
|
||||
*A note to PR reviewers: it may be helpful to review our [code review documentation](https://github.com/GSA/notifications-api/blob/main/docs/all.md#code-reviews) to know what to keep in mind while reviewing pull requests.*
|
||||
*A note to PR reviewers: it may be helpful to review our
|
||||
[code review documentation](https://github.com/GSA/notifications-api/blob/main/docs/all.md#code-reviews)
|
||||
to know what to keep in mind while reviewing pull requests.*
|
||||
|
||||
## Description
|
||||
|
||||
Please enter a detailed description here.
|
||||
Please enter a clear description about your proposed changes and what the
|
||||
expected outcome(s) is/are from there. If there are complex implementation
|
||||
details within the changes, this is a great place to explain those details using
|
||||
plain language.
|
||||
|
||||
This should include:
|
||||
|
||||
- Links to issues that this PR addresses
|
||||
- Screenshots or screen captures of any visible changes, especially for UI work
|
||||
- Dependency changes
|
||||
|
||||
If there are any caveats, known issues, follow-up items, etc., make a quick note
|
||||
of them here as well, though more details are probably warranted in the issue
|
||||
itself in this case.
|
||||
|
||||
## TODO (optional)
|
||||
|
||||
* [ ] TODO item 1
|
||||
* [ ] TODO item 2
|
||||
* [ ] TODO item ...
|
||||
If you're opening a draft PR, it might be helpful to list any outstanding work,
|
||||
especially if you're asking folks to take a look before it's ready for full
|
||||
review. In this case, create a small checklist with the outstanding items:
|
||||
|
||||
- [ ] TODO item 1
|
||||
- [ ] TODO item 2
|
||||
- [ ] TODO item ...
|
||||
|
||||
## Security Considerations
|
||||
|
||||
* Consideration 1
|
||||
* Consideration 2
|
||||
* Consideration ...
|
||||
Please think about the security compliance aspect of your changes and what the
|
||||
potential impacts might be.
|
||||
|
||||
**NOTE: Please be mindful of sharing sensitive information here! If you're not
|
||||
sure of what to write, please ask the team first before writing anything here.**
|
||||
|
||||
Relevant details could include (and are not limited to) the following:
|
||||
|
||||
- Handling secrets/credential management (or specifically calling out that there
|
||||
is nothing to handle)
|
||||
- Any adjustments to the flow of data in and out the system, or even within it
|
||||
- Connecting or disconnecting any external services to the application
|
||||
- Handling of any sensitive information, such as PII
|
||||
- Handling of information within log statements or other application monitoring
|
||||
services/hooks
|
||||
- The inclusion of a new external dependency or the removal of an existing one
|
||||
- ... (anything else relevant from a security compliance perspective)
|
||||
|
||||
There are some cases where there are no security considerations to be had, e.g.,
|
||||
updating our documentation with publicly available information. In those cases
|
||||
it is fine to simply put something like this:
|
||||
|
||||
- None; this is a documentation update with publicly available information.
|
||||
|
||||
86
.github/workflows/checks.yml
vendored
@@ -23,12 +23,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22.3.0"
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
@@ -44,6 +38,8 @@ 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
|
||||
@@ -52,14 +48,11 @@ jobs:
|
||||
run: poetry run coverage report --fail-under=90
|
||||
|
||||
end-to-end-tests:
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
@@ -85,39 +78,64 @@ jobs:
|
||||
ports:
|
||||
# Maps tcp port 6379 on service container to the host
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-project
|
||||
- uses: jwalton/gh-find-current-pr@v1
|
||||
id: findPr
|
||||
- name: Check API Server availability
|
||||
run: |
|
||||
curl --fail -v https://notify-api-staging.app.cloud.gov || exit 1
|
||||
- name: Run Admin server
|
||||
# If we want to log stuff and see what's broken,
|
||||
# insert this line:
|
||||
# tail -f admin-server.log &
|
||||
# above make e2e-test
|
||||
|
||||
|
||||
run: |
|
||||
make run-flask > admin-server.log 2>&1 &
|
||||
tail -f admin-server.log &
|
||||
make e2e-test
|
||||
|
||||
- name: Clone API
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: GSA/notifications-api
|
||||
path: 'notifications-api'
|
||||
- name: Install API dependencies
|
||||
working-directory: 'notifications-api'
|
||||
run: make bootstrap
|
||||
env:
|
||||
API_HOST_NAME: https://notify-api-staging.app.cloud.gov/
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
|
||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||
REDIS_URL: redis://localhost:6379
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_ENVIRONMENT: development
|
||||
- name: Run API server
|
||||
working-directory: 'notifications-api'
|
||||
run: make run-procfile &
|
||||
env:
|
||||
DATABASE_URL: postgresql://user:password@localhost:5432/test_notification_api
|
||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||
REDIS_URL: redis://localhost:6379
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_ENVIRONMENT: development
|
||||
- name: Run Admin server
|
||||
run: make run-flask &
|
||||
env:
|
||||
# API_HOST_NAME: https://notify-api-staging.app.cloud.gov
|
||||
API_HOST_NAME: http://localhost:6011
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
ADMIN_CLIENT_USERNAME: notify-admin
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
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 }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012
|
||||
- name: Run E2E tests
|
||||
# Run the E2E tests against the code found in this PR.
|
||||
# run: poetry run pytest -v --browser chromium --browser firefox --browser webkit tests/end_to_end
|
||||
# --browser webkit doesn't work at this time.
|
||||
run: make e2e-test
|
||||
# Debugging for now to troubleshoot a connectivity issue to the local servers
|
||||
# run: curl --request GET --url "http://localhost:6012"
|
||||
env:
|
||||
NOTIFY_ENVIRONMENT: e2etest
|
||||
NOTIFY_E2E_AUTH_STATE_PATH: ${{ secrets.NOTIFY_E2E_AUTH_STATE_PATH }}
|
||||
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
|
||||
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
|
||||
NOTIFY_E2E_TEST_URI: http://localhost:6012
|
||||
|
||||
validate-new-relic-config:
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
@@ -142,8 +160,6 @@ jobs:
|
||||
- uses: pypa/gh-action-pip-audit@v1.0.8
|
||||
with:
|
||||
inputs: requirements.txt
|
||||
ignore-vulns: |
|
||||
PYSEC-2024-60
|
||||
- name: Run npm audit
|
||||
run: make npm-audit
|
||||
|
||||
@@ -167,12 +183,12 @@ 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
|
||||
|
||||
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}}"
|
||||
25
.github/workflows/deploy-demo.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
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,12 +47,11 @@ 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 }}
|
||||
@@ -64,22 +63,21 @@ jobs:
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-demo.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-demo.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-demo
|
||||
cf_command: >-
|
||||
push -f manifest.yml
|
||||
push_arguments: >-
|
||||
--vars-file deploy-config/demo.yml
|
||||
--var DANGEROUS_SALT="$DANGEROUS_SALT"
|
||||
--var SECRET_KEY="$SECRET_KEY"
|
||||
--var ADMIN_CLIENT_USERNAME="notify-admin"
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
@@ -87,13 +85,10 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-demo.*.acl
|
||||
@@ -102,10 +97,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
|
||||
|
||||
25
.github/workflows/deploy-prod.yml
vendored
@@ -18,11 +18,11 @@ jobs:
|
||||
|
||||
- name: Check for changes to Terraform
|
||||
id: changed-terraform-files
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
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,12 +47,11 @@ 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 }}
|
||||
@@ -64,22 +63,21 @@ jobs:
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://beta.notify.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://beta.notify.gov/sign-in&response_type=code&scope=openid+email&state=STATE"
|
||||
LOGIN_DOT_GOV_CERTS_URL: "https://secure.login.gov/api/openid_connect/certs"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-production
|
||||
cf_command: >-
|
||||
push -f manifest.yml
|
||||
push_arguments: >-
|
||||
--vars-file deploy-config/production.yml
|
||||
--var DANGEROUS_SALT="$DANGEROUS_SALT"
|
||||
--var SECRET_KEY="$SECRET_KEY"
|
||||
--var ADMIN_CLIENT_USERNAME="notify-admin"
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var ADMIN_CLIENT_USERNAME="$ADMIN_CLIENT_USERNAME"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
@@ -87,13 +85,10 @@ jobs:
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
--var LOGIN_DOT_GOV_CERTS_URL="$LOGIN_DOT_GOV_CERTS_URL"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--strategy rolling
|
||||
|
||||
- name: Check for changes to egress config
|
||||
id: changed-egress-config
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v41.0.0
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-production.*.acl
|
||||
@@ -102,10 +97,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
|
||||
|
||||
175
.github/workflows/deploy.yml
vendored
@@ -17,105 +17,96 @@ jobs:
|
||||
|
||||
environment: staging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v4
|
||||
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@v41.0.0
|
||||
with:
|
||||
files: |
|
||||
terraform/staging
|
||||
terraform/shared
|
||||
.github/workflows/deploy.yml
|
||||
- name: Terraform init
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/staging
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
run: terraform init
|
||||
- name: Terraform apply
|
||||
if: steps.changed-terraform-files.outputs.any_changed == 'true'
|
||||
working-directory: terraform/staging
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
|
||||
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
run: terraform apply -auto-approve -input=false
|
||||
|
||||
- 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: 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: Deploy to cloud.gov
|
||||
uses: 18f/cg-deploy-action@main
|
||||
env:
|
||||
DANGEROUS_SALT: ${{ secrets.DANGEROUS_SALT }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }}
|
||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||
NR_BROWSER_KEY: ${{ secrets.NR_BROWSER_KEY }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
LOGIN_PEM: ${{ secrets.LOGIN_PEM }}
|
||||
LOGIN_DOT_GOV_CLIENT_ID: "urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov"
|
||||
LOGIN_DOT_GOV_USER_INFO_URL: "https://secure.login.gov/api/openid_connect/userinfo"
|
||||
LOGIN_DOT_GOV_ACCESS_TOKEN_URL: "https://secure.login.gov/api/openid_connect/token"
|
||||
LOGIN_DOT_GOV_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&post_logout_redirect_uri=https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_BASE_LOGOUT_URL: "https://secure.login.gov/openid_connect/logout?"
|
||||
LOGIN_DOT_GOV_SIGNOUT_REDIRECT: "https://notify-staging.app.cloud.gov/sign-out"
|
||||
LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/sign-in&response_type=code&scope=openid+email&state=STATEE"
|
||||
with:
|
||||
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}
|
||||
cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
|
||||
cf_org: gsa-tts-benefits-studio
|
||||
cf_space: notify-staging
|
||||
push_arguments: >-
|
||||
--vars-file deploy-config/staging.yml
|
||||
--var DANGEROUS_SALT="$DANGEROUS_SALT"
|
||||
--var SECRET_KEY="$SECRET_KEY"
|
||||
--var ADMIN_CLIENT_USERNAME="notify-admin"
|
||||
--var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET"
|
||||
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
|
||||
--var NR_BROWSER_KEY="$NR_BROWSER_KEY"
|
||||
--var COMMIT_HASH="$COMMIT_HASH"
|
||||
--var LOGIN_PEM="$LOGIN_PEM"
|
||||
--var LOGIN_DOT_GOV_CLIENT_ID="$LOGIN_DOT_GOV_CLIENT_ID"
|
||||
--var LOGIN_DOT_GOV_USER_INFO_URL="$LOGIN_DOT_GOV_USER_INFO_URL"
|
||||
--var LOGIN_DOT_GOV_ACCESS_TOKEN_URL="$LOGIN_DOT_GOV_ACCESS_TOKEN_URL"
|
||||
--var LOGIN_DOT_GOV_LOGOUT_URL="$LOGIN_DOT_GOV_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_BASE_LOGOUT_URL="$LOGIN_DOT_GOV_BASE_LOGOUT_URL"
|
||||
--var LOGIN_DOT_GOV_SIGNOUT_REDIRECT="$LOGIN_DOT_GOV_SIGNOUT_REDIRECT"
|
||||
--var LOGIN_DOT_GOV_INITIAL_SIGNIN_URL="$LOGIN_DOT_GOV_INITIAL_SIGNIN_URL"
|
||||
|
||||
|
||||
- name: 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@v41.0.0
|
||||
with:
|
||||
files: |
|
||||
deploy-config/egress_proxy/notify-admin-staging.*.acl
|
||||
.github/actions/deploy-proxy/action.yml
|
||||
.github/workflows/deploy.yml
|
||||
- name: Deploy egress proxy
|
||||
if: steps.changed-egress-config.outputs.any_changed == 'true'
|
||||
uses: ./.github/actions/deploy-proxy
|
||||
with:
|
||||
cf_space: notify-staging
|
||||
app: notify-admin-staging
|
||||
|
||||
bail:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
8
.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,8 +123,6 @@ app/templates/vendor
|
||||
secrets.auto.tfvars
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
requirements.txt
|
||||
terraform/sandbox/requirements.txt
|
||||
|
||||
# Playwright
|
||||
playwright/
|
||||
@@ -135,8 +132,3 @@ playwright/
|
||||
|
||||
# Nodenv
|
||||
.node-version
|
||||
|
||||
# BackstopJS
|
||||
backstop_data/bitmaps_test/
|
||||
backstop_data/html_report/
|
||||
backstop_data/engine_scripts/playwright/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -11,14 +11,3 @@ repos:
|
||||
- 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
|
||||
}
|
||||
14
Makefile
@@ -21,8 +21,7 @@ bootstrap: generate-version-file ## Set up everything to run the app
|
||||
poetry install --sync --no-root
|
||||
poetry run playwright install --with-deps
|
||||
poetry run pre-commit install
|
||||
source $(NVMSH) --no-use && nvm install && npm install
|
||||
source $(NVMSH) && npm ci --no-audit
|
||||
source $(NVMSH) --no-use && nvm install && npm ci --no-audit
|
||||
source $(NVMSH) && npm run build
|
||||
|
||||
.PHONY: watch-frontend
|
||||
@@ -41,10 +40,6 @@ run-flask-bare: ## Run flask without invoking poetry so we can override ENV var
|
||||
npm-audit: ## Check for vulnerabilities in NPM packages
|
||||
source $(NVMSH) && npm run audit
|
||||
|
||||
.PHONY: npm-audit-fix
|
||||
npm-audit-fix: ## Fix vulnerabilities that do not require attentino (according to npm)
|
||||
source $(NVMSH) && npm audit fix
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
@@ -81,14 +76,13 @@ py-test: ## Run python unit tests
|
||||
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
|
||||
poetry run pytest -vv --browser chromium --browser firefox tests/end_to_end
|
||||
|
||||
.PHONY: js-lint
|
||||
js-lint: ## Run javascript linting scanners
|
||||
|
||||
50
README.md
@@ -40,7 +40,7 @@ You will need the following items:
|
||||
[Follow the instructions here to set up the Notify.gov API.](https://github.com/GSA/notifications-api#before-you-start)
|
||||
|
||||
The Notify.gov API is required in order for the Notify.gov Admin UI to run, and
|
||||
it will also take care of many of the steps that are listed here. The sections
|
||||
it will also take care of many of the steps that are listed here. The sections
|
||||
that are a repeat from the API setup are flagged with an **[API Step]** label
|
||||
in front of them.
|
||||
|
||||
@@ -48,7 +48,7 @@ in front of them.
|
||||
|
||||
This project is set up to work with
|
||||
[nvm (Node Version Manager)](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
for managing and using Node.js (version 22.3.0) and the `npm` package manager.
|
||||
for managing and using Node.js (version 16.15.1) and the `npm` package manager.
|
||||
|
||||
These instructions will walk you through how to set your machine up with all of
|
||||
the required tools for this project.
|
||||
@@ -83,13 +83,11 @@ Your system `$PATH` environment variable is likely set in one of these
|
||||
locations:
|
||||
|
||||
For BASH shells:
|
||||
|
||||
- `~/.bashrc`
|
||||
- `~/.bash_profile`
|
||||
- `~/.profile`
|
||||
|
||||
For ZSH shells:
|
||||
|
||||
- `~/.zshrc`
|
||||
- `~/.zprofile`
|
||||
|
||||
@@ -99,7 +97,7 @@ environments.
|
||||
Which file you need to modify depends on whether or not you are running an
|
||||
interactive shell or a login shell
|
||||
(see [this Stack Overflow post](https://stackoverflow.com/questions/18186929/what-are-the-differences-between-a-login-shell-and-interactive-shell)
|
||||
for an explanation of the differences). If you're still not sure, please ask
|
||||
for an explanation of the differences). If you're still not sure, please ask
|
||||
the team for help!
|
||||
|
||||
Once you determine which file you'll need to modify, add these lines before any
|
||||
@@ -160,7 +158,7 @@ _NOTE: This project currently uses the latest `1.4.x release of Terraform._
|
||||
#### [API Step] Python Installation
|
||||
|
||||
Now we're going to install a tool to help us manage Python versions and
|
||||
virtual environments on our system. First, we'll install
|
||||
virtual environments on our system. First, we'll install
|
||||
[pyenv](https://github.com/pyenv/pyenv) and one of its plugins,
|
||||
[pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv), with Homebrew:
|
||||
|
||||
@@ -287,7 +285,7 @@ we'll use `3.12` in our example here since we recently upgraded to this version:
|
||||
pyenv install 3.12
|
||||
```
|
||||
|
||||
Next, delete the virtual environment you previously had set up. If you followed
|
||||
Next, delete the virtual environment you previously had set up. If you followed
|
||||
the instructions above with the first-time set up, you can do this with `pyenv`:
|
||||
|
||||
```sh
|
||||
@@ -308,20 +306,6 @@ you'll be set with an upgraded version of Python.
|
||||
|
||||
_If you're not sure about the details of your current virtual environment, you can run `poetry env info` to get more information. If you've been using `pyenv` for everything, you can also see all available virtual environments with `pyenv virtualenvs`._
|
||||
|
||||
#### 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
|
||||
@@ -369,12 +353,18 @@ 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
|
||||
will not be a user in the database to link to the login.gov account you are using. So
|
||||
you will need to create that user in your database using the 'create-test-user' command.
|
||||
|
||||
Open a terminal pointing to the api project and then run this command.
|
||||
Open two terminals pointing to the api project and then run these commands in the
|
||||
respective terminals.
|
||||
|
||||
```poetry run flask command create-test-user --admin=True```
|
||||
(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.
|
||||
@@ -382,6 +372,8 @@ is the same one you are using in login.gov and make sure your phone number is in
|
||||
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
|
||||
@@ -501,13 +493,3 @@ insurance.
|
||||
For more information on what we're working on, the Notify tool, and how to get
|
||||
involved with our team,
|
||||
[see our flyer.](https://github.com/GSA/notifications-admin/blob/main/docs/notify-pilot-flyer.md)
|
||||
|
||||
## Updating secrets for the E2E tests
|
||||
|
||||
At some point, E2E tests will fail because the secrets held in VCAP_SERVICES have expired. To refresh
|
||||
them, you will need to do the following:
|
||||
|
||||
1. Log in the normal way to access cloudfoundry command line options
|
||||
2. In your terminal, run `chmod +x print_vcap.sh`
|
||||
3. In your terminal, run `./print_vcap.sh`
|
||||
4. Copy the value in your terminal and paste it into the VCAP_SERVICES secret in Github on the staging tier.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
Contact: mailto:notify-support@gsa.gov
|
||||
Expires: 2035-10-15T23:59:59Z
|
||||
@@ -30,7 +30,7 @@ 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,
|
||||
@@ -119,6 +119,7 @@ login_manager = LoginManager()
|
||||
csrf = CSRFProtect()
|
||||
talisman = Talisman()
|
||||
|
||||
|
||||
# The current service attached to the request stack.
|
||||
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
||||
|
||||
@@ -162,17 +163,6 @@ def _csp(config):
|
||||
|
||||
|
||||
def create_app(application):
|
||||
application.config["FEATURE_BEST_PRACTICES_ENABLED"] = (
|
||||
os.getenv("FEATURE_BEST_PRACTICES_ENABLED", "false").lower() == "true"
|
||||
)
|
||||
|
||||
@application.context_processor
|
||||
def inject_feature_flags():
|
||||
feature_best_practices_enabled = application.config[
|
||||
"FEATURE_BEST_PRACTICES_ENABLED"
|
||||
]
|
||||
return dict(FEATURE_BEST_PRACTICES_ENABLED=feature_best_practices_enabled)
|
||||
|
||||
notify_environment = os.environ["NOTIFY_ENVIRONMENT"]
|
||||
|
||||
application.config.from_object(configs[notify_environment])
|
||||
@@ -212,6 +202,7 @@ def create_app(application):
|
||||
user_api_client,
|
||||
# External API clients
|
||||
redis_client,
|
||||
zendesk_client,
|
||||
):
|
||||
client.init_app(application)
|
||||
|
||||
@@ -240,24 +231,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
|
||||
@@ -375,23 +348,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():
|
||||
@@ -399,7 +360,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)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
href="/static/images/apple-touch-icon.png?a0f7e1b728a42016b247dc54ee40d055">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{ asset_url('images/site.webmanifest') }}">
|
||||
<link rel="manifest" href="/static/images/site.webmanifest">
|
||||
|
||||
<link rel="stylesheet" media="screen" href="/static/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
|
||||
<link rel="stylesheet" media="print" href="/static/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
|
||||
@@ -68,8 +68,11 @@
|
||||
<p class="usa-body">
|
||||
Try again later.
|
||||
</p>
|
||||
<p>To report a problem you can email us at <a class="usa-link" href="mailto:notify-support@gsa.gov">notify-support@gsa.gov</a>.</p>
|
||||
<p>You can expect a response within one business day.</p>
|
||||
<p class="usa-body">
|
||||
You can check our <a class="usa-link"
|
||||
href="https://status.notifications.service.gov.uk/">system status</a> page to see if there are any known
|
||||
issues.<br />To report a problem, email <a
|
||||
href="mailto:gov-uk-notify-support@digital.cabinet-office.gov.uk">gov-uk-notify-support@digital.cabinet-office.gov.uk</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.3 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 |
@@ -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: 7.4 KiB |
|
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);
|
||||
24
app/assets/javascripts/chartDashboard.js
Normal file
@@ -0,0 +1,24 @@
|
||||
(function (window) {
|
||||
|
||||
const ctx = document.getElementById('myChart');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
|
||||
datasets: [{
|
||||
label: '# of Votes',
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(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;
|
||||
}
|
||||
@@ -66,10 +66,6 @@ h2.sms-message-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.usa-prose >*+ h2.message-header {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
h2.recipient-list {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -344,3 +340,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,8 +22,9 @@ 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');
|
||||
iframe:focus, [href]:focus, [tabindex]:focus, [contentEditable=true]:focus {
|
||||
outline: units(1px) dotted color('blue-40v');
|
||||
outline: units(1px) auto color('blue-40v');
|
||||
outline-offset: 0.3rem;
|
||||
}
|
||||
|
||||
@@ -156,18 +157,6 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -276,12 +265,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;
|
||||
@@ -332,22 +315,6 @@ td.table-empty-message {
|
||||
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 {
|
||||
@@ -397,6 +364,9 @@ td.table-empty-message {
|
||||
background-image: url(../img/material-icons/description.svg);
|
||||
}
|
||||
}
|
||||
.table-wrapper {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-table {
|
||||
@@ -420,30 +390,6 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -463,51 +409,27 @@ td.table-empty-message {
|
||||
width: 25%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
td.jobid {
|
||||
width: 5%;
|
||||
}
|
||||
td.template {
|
||||
width: 25%;
|
||||
width: 20%;
|
||||
}
|
||||
td.time-sent {
|
||||
width: 15%;
|
||||
width: 20%;
|
||||
}
|
||||
td.sender {
|
||||
width: 20%;
|
||||
overflow-wrap: break-word;
|
||||
width: 15%;
|
||||
}
|
||||
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;
|
||||
width: 5%;
|
||||
}
|
||||
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;
|
||||
td {
|
||||
padding: 0.5rem 1rem
|
||||
}
|
||||
}
|
||||
|
||||
#template-list {
|
||||
@@ -522,10 +444,6 @@ td.table-empty-message {
|
||||
}
|
||||
}
|
||||
|
||||
.usa-prose > p.max-width-full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// Tabs
|
||||
|
||||
.tabs {
|
||||
@@ -671,248 +589,3 @@ details form {
|
||||
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;
|
||||
}
|
||||
|
||||
.about-icon-list {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 2px 1px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.usa-icon-list__content{
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.indented-paragraph {
|
||||
margin-left: calc(24px + 4px);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,5 @@ in the form $setting: value,
|
||||
$theme-banner-max-width: "desktop-lg",
|
||||
$theme-grid-container-max-width: "desktop-lg",
|
||||
$theme-footer-max-width: "desktop-lg",
|
||||
$theme-header-max-width: "desktop-lg",
|
||||
$theme-identifier-max-width: "desktop-lg"
|
||||
$theme-header-max-width: "desktop-lg"
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -8,7 +8,6 @@ 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")
|
||||
@@ -39,7 +38,7 @@ class Config(object):
|
||||
NR_MONITOR_ON = settings and settings.monitor_mode
|
||||
COMMIT_HASH = getenv("COMMIT_HASH", "--------")[0:7]
|
||||
|
||||
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov", "mil", "si.edu"]
|
||||
GOVERNMENT_EMAIL_DOMAIN_NAMES = ["gov"]
|
||||
|
||||
# Logging
|
||||
NOTIFY_LOG_LEVEL = getenv("NOTIFY_LOG_LEVEL", "INFO")
|
||||
@@ -54,13 +53,7 @@ class Config(object):
|
||||
PERMANENT_SESSION_LIFETIME = 1800 # 30 Minutes
|
||||
SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year
|
||||
REPLY_TO_EMAIL_ADDRESS_VALIDATION_TIMEOUT = 45
|
||||
ACTIVITY_STATS_LIMIT_DAYS = {
|
||||
"today": 0,
|
||||
"one_day": 1,
|
||||
"three_day": 3,
|
||||
"five_day": 5,
|
||||
"seven_day": 7,
|
||||
}
|
||||
ACTIVITY_STATS_LIMIT_DAYS = 7
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_NAME = "notify_admin_session"
|
||||
SESSION_COOKIE_SECURE = True
|
||||
@@ -87,13 +80,6 @@ class Config(object):
|
||||
"tts-benefits-studio@gsa.gov",
|
||||
],
|
||||
}
|
||||
FEATURE_BEST_PRACTICES_ENABLED = (
|
||||
getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true"
|
||||
)
|
||||
|
||||
FEATURE_ABOUT_PAGE_ENABLED = (
|
||||
getenv("FEATURE_ABOUT_PAGE_ENABLED", "false") == "true"
|
||||
)
|
||||
|
||||
|
||||
def _s3_credentials_from_env(bucket_prefix):
|
||||
|
||||
@@ -14,7 +14,7 @@ Learn how to [personalize messages](/using-notify/guidance) to increase response
|
||||
Learn about message _parts_ and [how limits are calculated](/using-notify/pricing).
|
||||
|
||||
5. ## Start sending messages
|
||||
To remove the restrictions of Trial Mode and begin sending messages to clients complete the <a class="usa-link usa-link--external" href="https://docs.google.com/forms/d/1fnaBtxuGf3q-OdGVyt2LqBKvp9_P21kmKJa0yIK8rWM/edit">Go-Live Form</a>.
|
||||
To remove the restrictions of Trial Mode and begin sending messages to clients complete the [Live Campaign Form](https://airtable.com/appe4n7jYOALPLcyU/shrIPWnLTw9U1fclL).
|
||||
We’ll respond within one business day.
|
||||
|
||||
### Questions?
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -231,11 +231,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)
|
||||
|
||||
@@ -3,7 +3,6 @@ from flask import Blueprint
|
||||
main = Blueprint("main", __name__)
|
||||
|
||||
from app.main.views import ( # noqa isort:skip
|
||||
activity,
|
||||
add_service,
|
||||
api_keys,
|
||||
choose_account,
|
||||
|
||||
@@ -50,7 +50,6 @@ from app.main.validators import (
|
||||
CommonlyUsedPassword,
|
||||
CsvFileValidator,
|
||||
DoesNotStartWithDoubleZero,
|
||||
FieldCannotContainComma,
|
||||
LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly,
|
||||
MustContainAlphanumericCharacters,
|
||||
NoCommasInPlaceHolders,
|
||||
@@ -1651,11 +1650,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
|
||||
|
||||
@@ -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"]
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
import calendar
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
@@ -11,12 +12,12 @@ from app import (
|
||||
billing_api_client,
|
||||
current_service,
|
||||
job_api_client,
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
template_statistics_client,
|
||||
)
|
||||
from app.formatters import format_date_numeric, format_datetime_numeric, get_time_left
|
||||
from app.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,
|
||||
@@ -40,7 +41,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,21 +48,19 @@ 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"]
|
||||
notifications_response = notification_api_client.get_notifications_for_service(
|
||||
service_id
|
||||
)["notifications"]
|
||||
service_data_retention_days = 7
|
||||
|
||||
jobs = [
|
||||
aggregate_notifications_by_job = defaultdict(list)
|
||||
for notification in notifications_response:
|
||||
job_id = notification.get("job", {}).get("id", None)
|
||||
if job_id:
|
||||
aggregate_notifications_by_job[job_id].append(notification)
|
||||
|
||||
job_and_notifications = [
|
||||
{
|
||||
"job_id": job["id"],
|
||||
"time_left": get_time_left(job["created_at"]),
|
||||
@@ -73,52 +71,22 @@ def service_dashboard(service_id):
|
||||
".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"],
|
||||
"notifications": aggregate_notifications_by_job.get(job["id"], []),
|
||||
}
|
||||
for job in job_response
|
||||
if job["job_status"] != "cancelled"
|
||||
if aggregate_notifications_by_job.get(job["id"], [])
|
||||
]
|
||||
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,
|
||||
job_and_notifications=job_and_notifications,
|
||||
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):
|
||||
@@ -325,19 +293,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"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -410,13 +372,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
|
||||
|
||||
|
||||
@@ -474,26 +434,6 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
def get_months_for_year(start, end, year):
|
||||
return [datetime(year, month, 1) for month in range(start, end)]
|
||||
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
import os
|
||||
import secrets
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import abort, current_app, redirect, render_template, request, url_for
|
||||
from flask import abort, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import redis_client, status_api_client
|
||||
from app import 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 (
|
||||
about_notify_nav,
|
||||
best_practices_nav,
|
||||
features_nav,
|
||||
using_notify_nav,
|
||||
)
|
||||
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 feature flags
|
||||
@main.before_request
|
||||
def check_feature_flags():
|
||||
if (
|
||||
request.path.startswith("/guides/best-practices")
|
||||
and not current_app.config.get("FEATURE_BEST_PRACTICES_ENABLED", False)
|
||||
):
|
||||
abort(404)
|
||||
|
||||
if (
|
||||
request.path.startswith("/about")
|
||||
and not current_app.config.get("FEATURE_ABOUT_PAGE_ENABLED", False)
|
||||
):
|
||||
abort(404)
|
||||
login_dot_gov_url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
|
||||
|
||||
@main.route("/")
|
||||
@@ -40,31 +18,11 @@ def index():
|
||||
if current_user and current_user.is_authenticated:
|
||||
return redirect(url_for("main.choose_account"))
|
||||
|
||||
ttl = 24 * 60 * 60
|
||||
|
||||
# make and store the state
|
||||
state = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state, ex=ttl)
|
||||
|
||||
# make and store the nonce
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce, ex=ttl)
|
||||
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", nonce)
|
||||
url = url.replace("STATE", state)
|
||||
return 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,
|
||||
login_dot_gov_url=login_dot_gov_url,
|
||||
)
|
||||
|
||||
|
||||
@@ -212,86 +170,12 @@ def trial_mode_new():
|
||||
)
|
||||
|
||||
|
||||
@main.route("/guides/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("/guides/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("/guides/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("/guides/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("/guides/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("/guides/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("/guides/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("/guides/using-notify/guidance")
|
||||
@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("/about")
|
||||
def about_notify():
|
||||
return render_template(
|
||||
"views/about/about.html",
|
||||
navigation_links=about_notify_nav(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -143,40 +143,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 +183,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 +212,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 +233,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(
|
||||
@@ -312,7 +286,7 @@ def get_status_filters(service, message_type, statistics):
|
||||
filters = [
|
||||
# key, label, option
|
||||
("requested", "total", "sending,delivered,failed"),
|
||||
("pending", "pending", "sending,pending"),
|
||||
("pending", "pending", "pending"),
|
||||
("delivered", "delivered", "delivered"),
|
||||
("failed", "failed", "failed"),
|
||||
]
|
||||
@@ -388,7 +362,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,7 +371,7 @@ 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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -137,9 +137,9 @@ 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()}"
|
||||
|
||||
@@ -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
|
||||
@@ -781,80 +735,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,7 +1,7 @@
|
||||
import base64
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import (
|
||||
abort,
|
||||
@@ -26,7 +26,6 @@ from app.main.views import sign_in
|
||||
from app.main.views.verify import activate_user
|
||||
from app.models.user import InvitedOrgUser, InvitedUser, User
|
||||
from app.utils import hide_from_search_engines, hilite
|
||||
from app.utils.user import is_gov_user
|
||||
|
||||
|
||||
@main.route("/register", methods=["GET", "POST"])
|
||||
@@ -116,10 +115,10 @@ def registration_continue():
|
||||
|
||||
def get_invite_data_from_redis(state):
|
||||
|
||||
invite_data = json.loads(redis_client.get(f"invitedata-{state}"))
|
||||
user_email = redis_client.get(f"user_email-{state}").decode("utf8")
|
||||
user_uuid = redis_client.get(f"user_uuid-{state}").decode("utf8")
|
||||
invited_user_email_address = redis_client.get(
|
||||
invite_data = json.loads(redis_client.raw_get(f"invitedata-{state}"))
|
||||
user_email = redis_client.raw_get(f"user_email-{state}").decode("utf8")
|
||||
user_uuid = redis_client.raw_get(f"user_uuid-{state}").decode("utf8")
|
||||
invited_user_email_address = redis_client.raw_get(
|
||||
f"invited_user_email_address-{state}"
|
||||
).decode("utf8")
|
||||
return invite_data, user_email, user_uuid, invited_user_email_address
|
||||
@@ -130,10 +129,10 @@ def put_invite_data_in_redis(
|
||||
):
|
||||
ttl = 60 * 15 # 15 minutes
|
||||
|
||||
redis_client.set(f"invitedata-{state}", json.dumps(invite_data), ex=ttl)
|
||||
redis_client.set(f"user_email-{state}", user_email, ex=ttl)
|
||||
redis_client.set(f"user_uuid-{state}", user_uuid, ex=ttl)
|
||||
redis_client.set(
|
||||
redis_client.raw_set(f"invitedata-{state}", json.dumps(invite_data), ex=ttl)
|
||||
redis_client.raw_set(f"user_email-{state}", user_email, ex=ttl)
|
||||
redis_client.raw_set(f"user_uuid-{state}", user_uuid, ex=ttl)
|
||||
redis_client.raw_set(
|
||||
f"invited_user_email_address-{state}",
|
||||
invited_user_email_address,
|
||||
ex=ttl,
|
||||
@@ -148,11 +147,6 @@ def check_invited_user_email_address_matches_expected(
|
||||
flash("You cannot accept an invite for another person.")
|
||||
abort(403)
|
||||
|
||||
if not is_gov_user(user_email):
|
||||
debug_msg("invited user has a non-government email address.")
|
||||
flash("You must use a government email address.")
|
||||
abort(403)
|
||||
|
||||
|
||||
@main.route("/set-up-your-profile", methods=["GET", "POST"])
|
||||
@hide_from_search_engines
|
||||
@@ -161,29 +155,17 @@ def set_up_your_profile():
|
||||
debug_msg(f"Enter set_up_your_profile with request.args {request.args}")
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
||||
if state != stored_state:
|
||||
current_app.logger.error(f"State Error: {state} != {stored_state}")
|
||||
abort(403)
|
||||
|
||||
login_gov_error = request.args.get("error")
|
||||
|
||||
user_email = redis_client.get(f"user_email-{state}")
|
||||
user_uuid = redis_client.get(f"user_uuid-{state}")
|
||||
|
||||
new_user = user_email is None or user_uuid is None
|
||||
|
||||
if new_user: # invite path
|
||||
access_token = sign_in._get_access_token(code)
|
||||
|
||||
if redis_client.raw_get(f"invitedata-{state}") is None:
|
||||
access_token = sign_in._get_access_token(code, state)
|
||||
debug_msg("Got the access token for login.gov")
|
||||
user_email, user_uuid = sign_in._get_user_email_and_uuid(access_token)
|
||||
debug_msg(
|
||||
f"Got the user_email {user_email} and user_uuid {user_uuid} from login.gov"
|
||||
)
|
||||
invite_data = redis_client.get(f"invitedata-{state}")
|
||||
invite_data = state.encode("utf8")
|
||||
invite_data = base64.b64decode(invite_data)
|
||||
invite_data = json.loads(invite_data)
|
||||
debug_msg(f"final state {invite_data}")
|
||||
invited_user_id = invite_data["invited_user_id"]
|
||||
@@ -205,7 +187,10 @@ def set_up_your_profile():
|
||||
|
||||
form = SetupUserProfileForm()
|
||||
|
||||
if form.validate_on_submit() and not new_user:
|
||||
if (
|
||||
form.validate_on_submit()
|
||||
and redis_client.raw_get(f"invitedata-{state}") is not None
|
||||
):
|
||||
invite_data, user_email, user_uuid, invited_user_email_address = (
|
||||
get_invite_data_from_redis(state)
|
||||
)
|
||||
@@ -230,7 +215,6 @@ def set_up_your_profile():
|
||||
activate_user(user["id"])
|
||||
debug_msg("activated user")
|
||||
usr = User.from_id(user["id"])
|
||||
|
||||
usr.add_to_service(
|
||||
invite_data["service_id"],
|
||||
invite_data["permissions"],
|
||||
@@ -240,12 +224,7 @@ def set_up_your_profile():
|
||||
debug_msg(
|
||||
f"Added user {usr.email_address} to service {invite_data['service_id']}"
|
||||
)
|
||||
# notify-admin-1766
|
||||
# redirect new users to templates area of new service instead of dashboard
|
||||
service_id = invite_data["service_id"]
|
||||
url = url_for(".service_dashboard", service_id=service_id)
|
||||
url = f"{url}/templates"
|
||||
return redirect(url)
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
|
||||
elif login_gov_error:
|
||||
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
||||
@@ -266,21 +245,10 @@ def get_invited_user_email_address(invited_user_id):
|
||||
|
||||
def invited_user_accept_invite(invited_user_id):
|
||||
invited_user = InvitedUser.by_id(invited_user_id)
|
||||
|
||||
if invited_user.status == "expired":
|
||||
current_app.logger.error("User invitation has expired")
|
||||
flash(
|
||||
"Your invitation has expired; please contact the person who invited you for additional help."
|
||||
)
|
||||
flash("Your invitation has expired.")
|
||||
abort(401)
|
||||
|
||||
if invited_user.status == "cancelled":
|
||||
current_app.logger.error("User invitation has been cancelled")
|
||||
flash(
|
||||
"Your invitation is no longer valid; please contact the person who invited you for additional help."
|
||||
)
|
||||
abort(401)
|
||||
|
||||
invited_user.accept_invite()
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import send_from_directory
|
||||
from flask import redirect
|
||||
|
||||
from app.main import main
|
||||
|
||||
@@ -6,4 +6,6 @@ from app.main import main
|
||||
@main.route("/.well-known/security.txt", methods=["GET"])
|
||||
@main.route("/security.txt", methods=["GET"])
|
||||
def security_policy():
|
||||
return send_from_directory(".well-known", "security.txt")
|
||||
# See GDS Way security policy which this implements
|
||||
# https://gds-way.cloudapps.digital/standards/vulnerability-disclosure.html#vulnerability-disclosure-and-security-txt
|
||||
return redirect("https://vdp.cabinetoffice.gov.uk/.well-known/security.txt")
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from string import ascii_uppercase
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from flask import (
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask import abort, flash, redirect, render_template, request, session, url_for
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
from notifications_python_client.errors import HTTPError
|
||||
@@ -34,7 +24,6 @@ from app.main.forms import (
|
||||
SetSenderForm,
|
||||
get_placeholder_form_instance,
|
||||
)
|
||||
from app.main.views.user_profile import set_timezone
|
||||
from app.models.user import Users
|
||||
from app.s3_client.s3_csv_client import (
|
||||
get_csv_metadata,
|
||||
@@ -42,12 +31,7 @@ from app.s3_client.s3_csv_client import (
|
||||
s3upload,
|
||||
set_metadata_on_csv_upload,
|
||||
)
|
||||
from app.utils import (
|
||||
PermanentRedirect,
|
||||
hilite,
|
||||
should_skip_template_page,
|
||||
unicode_truncate,
|
||||
)
|
||||
from app.utils import PermanentRedirect, should_skip_template_page, unicode_truncate
|
||||
from app.utils.csv import Spreadsheet, get_errors_for_csv
|
||||
from app.utils.templates import get_template
|
||||
from app.utils.user import user_has_permissions
|
||||
@@ -669,17 +653,18 @@ def preview_job(service_id, template_id, upload_id, row_index=2):
|
||||
data = _check_messages(
|
||||
service_id, template_id, upload_id, row_index, force_hide_sender=True
|
||||
)
|
||||
create_job(service_id, upload_id)
|
||||
|
||||
return render_template(
|
||||
"views/check/preview.html",
|
||||
scheduled_for=session["scheduled_for"],
|
||||
scheduled_for=session.get("scheduled_for"),
|
||||
**data,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
|
||||
@main.route("/services/<uuid:service_id>/create-job/<uuid:upload_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def start_job(service_id, upload_id):
|
||||
def create_job(service_id, upload_id):
|
||||
scheduled_for = session.pop("scheduled_for", None)
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
@@ -687,8 +672,12 @@ def start_job(service_id, upload_id):
|
||||
scheduled_for=scheduled_for,
|
||||
)
|
||||
|
||||
session.pop("sender_id", None)
|
||||
|
||||
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def start_job(service_id, upload_id):
|
||||
job_api_client.start_job(service_id, upload_id)
|
||||
session.pop("sender_id", None)
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.view_job",
|
||||
@@ -802,15 +791,9 @@ def get_skip_link(step_index, template):
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_one_off_to_myself(service_id, template_id):
|
||||
current_app.logger.info("Send one off to myself")
|
||||
try:
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
except Exception:
|
||||
current_app.logger.exception("Couldnt get template for one off")
|
||||
# Use 406 just because we're limited to certain codes here and it will point us back to a problem here
|
||||
abort(406)
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
)
|
||||
|
||||
if db_template["template_type"] not in ("sms", "email"):
|
||||
abort(404)
|
||||
@@ -930,7 +913,39 @@ def preview_notification(service_id, template_id):
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
scheduled_for = session.pop("scheduled_for", "")
|
||||
keys = []
|
||||
values = []
|
||||
for k, v in session["placeholders"].items():
|
||||
keys.append(k)
|
||||
values.append(v)
|
||||
|
||||
data = ",".join(keys)
|
||||
vals = ",".join(values)
|
||||
data = f"{data}\r\n{vals}"
|
||||
|
||||
filename = f"one-off-{current_user.name}-{uuid.uuid4()}.csv"
|
||||
my_data = {"filename": filename, "template_id": template_id, "data": data}
|
||||
upload_id = s3upload(service_id, my_data)
|
||||
form = CsvUploadForm()
|
||||
form.file.data = my_data
|
||||
form.file.name = filename
|
||||
|
||||
check_message_output = check_messages(service_id, template_id, upload_id, 2)
|
||||
if "You cannot send to" in check_message_output:
|
||||
return check_messages(service_id, template_id, upload_id, 2)
|
||||
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for=scheduled_for,
|
||||
template_id=template_id,
|
||||
original_file_name=filename,
|
||||
notification_count=1,
|
||||
valid="True",
|
||||
)
|
||||
session["recipient"] = recipient
|
||||
session["upload_id"] = upload_id
|
||||
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
||||
|
||||
return render_template(
|
||||
@@ -938,6 +953,7 @@ def preview_notification(service_id, template_id):
|
||||
**_check_notification(
|
||||
service_id, template_id, show_recipient=False, force_hide_sender=True
|
||||
),
|
||||
upload_id=upload_id,
|
||||
scheduled_for=session["scheduled_for"],
|
||||
recipient=recipient,
|
||||
)
|
||||
@@ -950,7 +966,6 @@ def preview_notification(service_id, template_id):
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
def send_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
|
||||
if not recipient:
|
||||
return redirect(
|
||||
url_for(
|
||||
@@ -959,11 +974,9 @@ def send_notification(service_id, template_id):
|
||||
template_id=template_id,
|
||||
)
|
||||
)
|
||||
upload_id = _send_notification(service_id, template_id)
|
||||
|
||||
session.pop("recipient", "")
|
||||
session.pop("placeholders", "")
|
||||
|
||||
upload_id = session.pop("upload_id")
|
||||
job_api_client.start_job(service_id, upload_id)
|
||||
# We have to wait for the job to run and create the notification in the database
|
||||
time.sleep(0.1)
|
||||
notifications = notification_api_client.get_notifications_for_service(
|
||||
@@ -992,17 +1005,15 @@ def send_notification(service_id, template_id):
|
||||
job_id=upload_id,
|
||||
)
|
||||
)
|
||||
total = notifications["total"]
|
||||
current_app.logger.info(
|
||||
hilite(
|
||||
f"job_id: {upload_id} has notifications: {total} and attempts: {attempts}"
|
||||
)
|
||||
)
|
||||
session.pop("recipient")
|
||||
session.pop("placeholders")
|
||||
return redirect(
|
||||
url_for(
|
||||
".view_job",
|
||||
service_id=service_id,
|
||||
job_id=upload_id,
|
||||
from_job=upload_id,
|
||||
notification_id=notifications["notifications"][0]["id"],
|
||||
# used to show the final step of the tour (help=3) or not show
|
||||
# a back link on a just sent one off notification (help=0)
|
||||
help=request.args.get("help"),
|
||||
@@ -1010,63 +1021,6 @@ def send_notification(service_id, template_id):
|
||||
)
|
||||
|
||||
|
||||
def _send_notification(service_id, template_id):
|
||||
scheduled_for = session.pop("scheduled_for", "")
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
# Guarantee that the real phone number comes last, because some
|
||||
# users will have placeholders like "add your second phone number"
|
||||
# or something like as custom placeholders.
|
||||
for k, v in session["placeholders"].items():
|
||||
if k != "phone number":
|
||||
keys.append(k)
|
||||
values.append(v)
|
||||
if "phone number" in session["placeholders"].keys():
|
||||
keys.append("phone number")
|
||||
values.append(session["placeholders"]["phone number"])
|
||||
|
||||
data = ",".join(keys)
|
||||
vals = ",".join(values)
|
||||
data = f"{data}\r\n{vals}"
|
||||
filename = (
|
||||
f"one-off-{uuid.uuid4()}.csv" # {current_user.name} removed from filename
|
||||
)
|
||||
my_data = {"filename": filename, "template_id": template_id, "data": data}
|
||||
upload_id = s3upload(service_id, my_data)
|
||||
# To debug messages that the user reports have not been sent, we log
|
||||
# the csv filename and the job id. The user will give us the file name,
|
||||
# so we can search on that to obtain the job id, which we can use elsewhere
|
||||
# on the API side to find out what happens to the message.
|
||||
current_app.logger.info(
|
||||
hilite(
|
||||
f"One-off file: {filename} job_id: {upload_id} s3 location: {service_id}-service-notify/{upload_id}.csv"
|
||||
)
|
||||
)
|
||||
|
||||
# For load testing we want to skip these checks. They are doing some fine-grained
|
||||
# comparison about what is in the preview, but the load test just blast messages
|
||||
# and doesn't care about the preview.
|
||||
if os.getenv("NOTIFY_ENVIRONMENT") not in ("development", "staging", "demo"):
|
||||
form = CsvUploadForm()
|
||||
form.file.data = my_data
|
||||
form.file.name = filename
|
||||
check_message_output = check_messages(service_id, template_id, upload_id, 2)
|
||||
if "You cannot send to" in check_message_output:
|
||||
return check_messages(service_id, template_id, upload_id, 2)
|
||||
|
||||
job_api_client.create_job(
|
||||
upload_id,
|
||||
service_id,
|
||||
scheduled_for=scheduled_for,
|
||||
template_id=template_id,
|
||||
original_file_name=filename,
|
||||
notification_count=1,
|
||||
valid="True",
|
||||
)
|
||||
return upload_id
|
||||
|
||||
|
||||
def get_email_reply_to_address_from_session():
|
||||
if session.get("sender_id"):
|
||||
return current_service.get_email_reply_to_address(session["sender_id"])[
|
||||
@@ -1075,13 +1029,8 @@ def get_email_reply_to_address_from_session():
|
||||
|
||||
|
||||
def get_sms_sender_from_session():
|
||||
sender_id = session.get("sender_id")
|
||||
if sender_id:
|
||||
sms_sender = current_service.get_sms_sender(session["sender_id"])["sms_sender"]
|
||||
current_app.logger.info(f"SMS Sender ({sender_id}) #: {sms_sender}")
|
||||
return sms_sender
|
||||
else:
|
||||
current_app.logger.error("No SMS Sender!!!!!!")
|
||||
if session.get("sender_id"):
|
||||
return current_service.get_sms_sender(session["sender_id"])["sms_sender"]
|
||||
|
||||
|
||||
def get_spreadsheet_column_headings_from_template(template):
|
||||
@@ -1097,7 +1046,6 @@ def get_spreadsheet_column_headings_from_template(template):
|
||||
|
||||
|
||||
def get_recipient():
|
||||
set_timezone()
|
||||
if {"recipient", "placeholders"} - set(session.keys()):
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,46 +1,36 @@
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import unquote
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
from flask import (
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
flash,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
from flask import Response, current_app, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import login_manager, redis_client, user_api_client
|
||||
from app import login_manager, user_api_client
|
||||
from app.main import main
|
||||
from app.main.views.index import error
|
||||
from app.main.views.verify import activate_user
|
||||
from app.models.user import User
|
||||
from app.utils import hide_from_search_engines
|
||||
from app.utils.login import get_id_token, is_safe_redirect_url
|
||||
|
||||
# from app.utils.time import is_less_than_days_ago
|
||||
from app.utils.user import is_gov_user
|
||||
from app.utils.login import is_safe_redirect_url
|
||||
from app.utils.time import is_less_than_days_ago
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
|
||||
def _reformat_keystring(orig): # pragma: no cover
|
||||
arr = orig.split("-----")
|
||||
begin = arr[1]
|
||||
end = arr[3]
|
||||
middle = arr[2].strip()
|
||||
new_keystring = f"-----{begin}-----\n{middle}\n-----{end}-----\n"
|
||||
def _reformat_keystring(orig):
|
||||
new_keystring = orig.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
new_keystring = new_keystring.replace("-----END PRIVATE KEY-----", "")
|
||||
new_keystring = new_keystring.strip()
|
||||
new_keystring = new_keystring.replace(" ", "\n")
|
||||
new_keystring = "\n".join(
|
||||
["-----BEGIN PRIVATE KEY-----", new_keystring, "-----END PRIVATE KEY-----"]
|
||||
)
|
||||
new_keystring = f"{new_keystring}\n"
|
||||
return new_keystring
|
||||
|
||||
|
||||
def _get_access_token(code): # pragma: no cover
|
||||
def _get_access_token(code, state):
|
||||
client_id = os.getenv("LOGIN_DOT_GOV_CLIENT_ID")
|
||||
access_token_url = os.getenv("LOGIN_DOT_GOV_ACCESS_TOKEN_URL")
|
||||
keystring = os.getenv("LOGIN_PEM")
|
||||
@@ -63,28 +53,15 @@ def _get_access_token(code): # pragma: no cover
|
||||
url = f"{base_url}{cli_assert}&{cli_assert_type}&{code_param}&grant_type=authorization_code"
|
||||
headers = {"Authorization": "Bearer %s" % token}
|
||||
response = requests.post(url, headers=headers)
|
||||
|
||||
response_json = response.json()
|
||||
id_token = get_id_token(response_json)
|
||||
nonce = id_token["nonce"]
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
stored_nonce = redis_client.get(nonce_key).decode("utf8")
|
||||
|
||||
if nonce != stored_nonce:
|
||||
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
|
||||
abort(403)
|
||||
|
||||
try:
|
||||
access_token = response_json["access_token"]
|
||||
except KeyError as e:
|
||||
current_app.logger.exception(
|
||||
f"Error when getting access token {response.json()} #notify-admin-1505"
|
||||
)
|
||||
raise KeyError(f"'access_token' {response.json()}") from e
|
||||
if response.json().get("access_token") is None:
|
||||
# Capture the response json here so it hopefully shows up in error reports
|
||||
current_app.logger.error(f"Error when getting access token {response.json()}")
|
||||
raise KeyError(f"'access_token' {response.json()}")
|
||||
access_token = response.json()["access_token"]
|
||||
return access_token
|
||||
|
||||
|
||||
def _get_user_email_and_uuid(access_token): # pragma: no cover
|
||||
def _get_user_email_and_uuid(access_token):
|
||||
headers = {"Authorization": "Bearer %s" % access_token}
|
||||
user_info_url = os.getenv("LOGIN_DOT_GOV_USER_INFO_URL")
|
||||
user_attributes = requests.get(
|
||||
@@ -96,70 +73,42 @@ def _get_user_email_and_uuid(access_token): # pragma: no cover
|
||||
return user_email, user_uuid
|
||||
|
||||
|
||||
def _do_login_dot_gov(): # $ pragma: no cover
|
||||
def _do_login_dot_gov():
|
||||
# start login.gov
|
||||
code = request.args.get("code")
|
||||
state = request.args.get("state")
|
||||
|
||||
login_gov_error = request.args.get("error")
|
||||
|
||||
if login_gov_error:
|
||||
current_app.logger.error(
|
||||
f"login.gov error: {login_gov_error} #notify-admin-1505"
|
||||
)
|
||||
current_app.logger.error(f"login.gov error: {login_gov_error}")
|
||||
raise Exception(f"Could not login with login.gov {login_gov_error}")
|
||||
elif code and state:
|
||||
verify_key = f"login-verify_email-{unquote(state)}"
|
||||
verify_path = bool(redis_client.get(verify_key))
|
||||
|
||||
if not verify_path:
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
|
||||
if state != stored_state:
|
||||
current_app.logger.error(f"State Error: {state} != {stored_state}")
|
||||
abort(403)
|
||||
|
||||
# activate the user
|
||||
try:
|
||||
access_token = _get_access_token(code)
|
||||
access_token = _get_access_token(code, state)
|
||||
user_email, user_uuid = _get_user_email_and_uuid(access_token)
|
||||
if not is_gov_user(user_email):
|
||||
current_app.logger.error(
|
||||
"invited user has a non-government email address. #notify-admin-1505"
|
||||
)
|
||||
flash("You must use a government email address.")
|
||||
abort(403)
|
||||
redirect_url = request.args.get("next")
|
||||
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
|
||||
current_app.logger.info(
|
||||
f"Retrieved user {user['id']} from db #notify-admin-1505"
|
||||
)
|
||||
|
||||
# Temporary disabling of this until we figure out what is happening.
|
||||
# # Check if the email needs to be revalidated
|
||||
# is_fresh_email = is_less_than_days_ago(
|
||||
# user["email_access_validated_at"], 90
|
||||
# )
|
||||
# if not is_fresh_email:
|
||||
# # send email verify
|
||||
# ttl = 24 * 60 * 60
|
||||
# verify_key = f"login-verify_email-{unquote(state)}"
|
||||
# redis_client.set(verify_key, state, ex=ttl)
|
||||
# return verify_email(user, redirect_url)
|
||||
# Check if the email needs to be revalidated
|
||||
is_fresh_email = is_less_than_days_ago(
|
||||
user["email_access_validated_at"], 90
|
||||
)
|
||||
if not is_fresh_email:
|
||||
return verify_email(user, redirect_url)
|
||||
|
||||
usr = User.from_email_address(user["email_address"])
|
||||
current_app.logger.info(f"activating user {usr.id} #notify-admin-1505")
|
||||
activate_user(usr.id)
|
||||
except BaseException as be: # noqa B036
|
||||
current_app.logger.error(f"Error signing in: {be} #notify-admin-1505 ")
|
||||
current_app.logger.error(be)
|
||||
error(401)
|
||||
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
|
||||
# end login.gov
|
||||
|
||||
|
||||
def verify_email(user, redirect_url): # pragma: no cover
|
||||
def verify_email(user, redirect_url):
|
||||
user_api_client.send_verify_code(user["id"], "email", None, redirect_url)
|
||||
title = "Email resent" if request.args.get("email_resent") else "Check your email"
|
||||
redirect_url = request.args.get("next")
|
||||
@@ -168,41 +117,19 @@ def verify_email(user, redirect_url): # pragma: no cover
|
||||
)
|
||||
|
||||
|
||||
def _handle_e2e_tests(redirect_url): # pragma: no cover
|
||||
try:
|
||||
current_app.logger.warning("E2E TESTS ARE ENABLED.")
|
||||
current_app.logger.warning(
|
||||
"If you are getting a 404 on signin, comment out E2E vars in .env file!"
|
||||
)
|
||||
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
|
||||
activate_user(user["id"])
|
||||
|
||||
# Check if the redirect URL is present and safe before proceeding further
|
||||
if redirect_url and is_safe_redirect_url(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"main.show_accounts_or_dashboard",
|
||||
next="EMAIL_IS_OK",
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
stre = str(e)
|
||||
stre = stre.replace(" ", "_")
|
||||
# Trying to get a message back to playwright somehow since we can't raise an error
|
||||
return redirect(url_for(f"https://{stre}"))
|
||||
def _handle_e2e_tests(redirect_url):
|
||||
current_app.logger.warning("E2E TESTS ARE ENABLED.")
|
||||
current_app.logger.warning(
|
||||
"If you are getting a 404 on signin, comment out E2E vars in .env file!"
|
||||
)
|
||||
user = user_api_client.get_user_by_email(os.getenv("NOTIFY_E2E_TEST_EMAIL"))
|
||||
activate_user(user["id"])
|
||||
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
|
||||
|
||||
|
||||
@main.route("/sign-in", methods=(["GET", "POST"]))
|
||||
@hide_from_search_engines
|
||||
def sign_in(): # pragma: no cover
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
return _handle_e2e_tests(redirect_url)
|
||||
|
||||
def sign_in():
|
||||
# If we have to revalidated the email, send the message
|
||||
# via email and redirect to the "verify your email page"
|
||||
# and don't proceed further with login
|
||||
@@ -214,31 +141,26 @@ def sign_in(): # pragma: no cover
|
||||
):
|
||||
return email_verify_template
|
||||
|
||||
redirect_url = request.args.get("next")
|
||||
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
return _handle_e2e_tests(redirect_url)
|
||||
|
||||
if current_user and current_user.is_authenticated:
|
||||
if redirect_url and is_safe_redirect_url(redirect_url):
|
||||
return redirect(redirect_url)
|
||||
return redirect(url_for("main.show_accounts_or_dashboard"))
|
||||
|
||||
ttl = 24 * 60 * 60
|
||||
|
||||
state = generate_token(
|
||||
token = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state, ex=ttl)
|
||||
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce, ex=ttl)
|
||||
|
||||
url = os.getenv("LOGIN_DOT_GOV_INITIAL_SIGNIN_URL")
|
||||
# handle unit tests
|
||||
if url is not None:
|
||||
url = url.replace("NONCE", nonce)
|
||||
url = url.replace("STATE", state)
|
||||
|
||||
url = url.replace("NONCE", token)
|
||||
url = url.replace("STATE", token)
|
||||
return render_template(
|
||||
"views/signin.html",
|
||||
again=bool(redirect_url),
|
||||
@@ -247,5 +169,5 @@ def sign_in(): # pragma: no cover
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def sign_in_again(): # pragma: no cover
|
||||
def sign_in_again():
|
||||
return redirect(url_for("main.sign_in", next=request.path))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
from flask import current_app, redirect, session, url_for
|
||||
from flask import current_app, redirect, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app.main import main
|
||||
@@ -25,16 +25,12 @@ def _sign_out_at_login_dot_gov():
|
||||
|
||||
@main.route("/sign-out", methods=(["GET", "POST"]))
|
||||
def sign_out():
|
||||
|
||||
# An AnonymousUser does not have an id
|
||||
current_app.logger.info("HIT THE REGULAR SIGN OUT")
|
||||
if current_user.is_authenticated:
|
||||
current_user.deactivate()
|
||||
session.clear()
|
||||
# TODO This doesn't work yet, due to problems above.
|
||||
current_user.sign_out()
|
||||
|
||||
session.permanent = False
|
||||
|
||||
login_dot_gov_logout_url = os.getenv("LOGIN_DOT_GOV_LOGOUT_URL")
|
||||
if login_dot_gov_logout_url:
|
||||
current_app.config["SESSION_PERMANENT"] = False
|
||||
return redirect(login_dot_gov_logout_url)
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
@@ -27,16 +27,12 @@ def using_notify_nav():
|
||||
"name": "Get started",
|
||||
"link": "main.get_started",
|
||||
},
|
||||
{
|
||||
"name": "Guides",
|
||||
"link": "main.best_practices",
|
||||
},
|
||||
{
|
||||
"name": "Trial mode",
|
||||
"link": "main.trial_mode_new",
|
||||
},
|
||||
{
|
||||
"name": "Tracking usage",
|
||||
"name": "Pricing",
|
||||
"link": "main.pricing",
|
||||
},
|
||||
{
|
||||
@@ -62,55 +58,3 @@ def using_notify_nav():
|
||||
# },
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def best_practices_nav():
|
||||
return [
|
||||
{
|
||||
"name": "Best Practices",
|
||||
"link": "main.best_practices",
|
||||
},
|
||||
{
|
||||
"name": "Clear goals",
|
||||
"link": "main.clear_goals",
|
||||
},
|
||||
{
|
||||
"name": "Rules and regulations",
|
||||
"link": "main.rules_and_regulations",
|
||||
},
|
||||
{
|
||||
"name": "Establish trust",
|
||||
"link": "main.establish_trust",
|
||||
"sub_navigation_items": [
|
||||
{
|
||||
"name": "Get the word out",
|
||||
"link": "main.establish_trust#get-the-word-out",
|
||||
},
|
||||
{
|
||||
"name": "As people receive texts",
|
||||
"link": "main.establish_trust#as-people-receive-texts",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Write for action",
|
||||
"link": "main.write_for_action",
|
||||
},
|
||||
{
|
||||
"name": "Multiple languages",
|
||||
"link": "main.multiple_languages",
|
||||
},
|
||||
{
|
||||
"name": "Benchmark performance",
|
||||
"link": "main.benchmark_performance",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def about_notify_nav():
|
||||
return [
|
||||
{
|
||||
"name": "About notify",
|
||||
"link": "main.about_notify",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -195,10 +195,10 @@ def check_tour_notification(service_id, template_id):
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"views/notifications/preview.html",
|
||||
"views/notifications/check.html",
|
||||
template=template,
|
||||
back_link=back_link,
|
||||
help="3",
|
||||
help="2",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ from app.main.forms import (
|
||||
TwoFactorForm,
|
||||
)
|
||||
from app.models.user import User
|
||||
from app.utils import hilite
|
||||
from app.utils.user import user_is_gov_user, user_is_logged_in
|
||||
from notifications_utils.url_safe_token import check_token
|
||||
|
||||
@@ -190,19 +189,32 @@ def user_profile_mobile_number_delete():
|
||||
@main.route("/user-profile/mobile-number/authenticate", methods=["GET", "POST"])
|
||||
@user_is_logged_in
|
||||
def user_profile_mobile_number_authenticate():
|
||||
# Validate password for form
|
||||
def _check_password(pwd):
|
||||
return user_api_client.verify_password(current_user.id, pwd)
|
||||
|
||||
form = ConfirmPasswordForm(_check_password)
|
||||
|
||||
if NEW_MOBILE not in session:
|
||||
return redirect(url_for(".user_profile_mobile_number"))
|
||||
|
||||
session[NEW_MOBILE_PASSWORD_CONFIRMED] = True
|
||||
current_user.send_verify_code(to=session[NEW_MOBILE])
|
||||
create_mobile_number_change_event(
|
||||
user_id=current_user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_mobile_number=current_user.mobile_number,
|
||||
new_mobile_number=session[NEW_MOBILE],
|
||||
if form.validate_on_submit():
|
||||
session[NEW_MOBILE_PASSWORD_CONFIRMED] = True
|
||||
current_user.send_verify_code(to=session[NEW_MOBILE])
|
||||
create_mobile_number_change_event(
|
||||
user_id=current_user.id,
|
||||
updated_by_id=current_user.id,
|
||||
original_mobile_number=current_user.mobile_number,
|
||||
new_mobile_number=session[NEW_MOBILE],
|
||||
)
|
||||
return redirect(url_for(".user_profile_mobile_number_confirm"))
|
||||
|
||||
return render_template(
|
||||
"views/user-profile/authenticate.html",
|
||||
thing="mobile number",
|
||||
form=form,
|
||||
back_link=url_for(".user_profile_mobile_number_confirm"),
|
||||
)
|
||||
return redirect(url_for(".user_profile_mobile_number_confirm"))
|
||||
|
||||
|
||||
@main.route("/user-profile/mobile-number/confirm", methods=["GET", "POST"])
|
||||
@@ -276,15 +288,3 @@ def user_profile_disable_platform_admin_view():
|
||||
return render_template(
|
||||
"views/user-profile/disable-platform-admin-view.html", form=form
|
||||
)
|
||||
|
||||
|
||||
def set_timezone():
|
||||
# Cookie is set in dashboard.html on page load
|
||||
try:
|
||||
timezone = request.cookies.get("timezone", "US/Eastern")
|
||||
current_app.logger.debug(hilite(f"User's timezone is {timezone}"))
|
||||
serialized_user = current_user.serialize()
|
||||
if serialized_user["preferred_timezone"] is not timezone:
|
||||
current_user.update(preferred_timezone=timezone)
|
||||
except Exception:
|
||||
current_app.logger.exception(hilite("Can't get timezone"))
|
||||
|
||||
@@ -38,7 +38,6 @@ def verify_email(token):
|
||||
current_app.config["EMAIL_EXPIRY_SECONDS"],
|
||||
)
|
||||
except SignatureExpired:
|
||||
current_app.logger.error("Email link expired #notify-admin-1505")
|
||||
flash(
|
||||
"The link in the email we sent you has expired. We've sent you a new one."
|
||||
)
|
||||
@@ -51,9 +50,6 @@ def verify_email(token):
|
||||
abort(404)
|
||||
|
||||
if user.is_active:
|
||||
current_app.logger.error(
|
||||
f"User is using an invite link but is already logged in {user.id} #notify-admin-1505"
|
||||
)
|
||||
flash("That verification link has expired.")
|
||||
return redirect(url_for("main.sign_in"))
|
||||
|
||||
@@ -63,7 +59,6 @@ def verify_email(token):
|
||||
|
||||
user.send_verify_code()
|
||||
session["user_details"] = {"email": user.email_address, "id": user.id}
|
||||
current_app.logger.info(f"Email verified for user {user.id} #notify-admin-1505")
|
||||
return redirect(url_for("main.verify"))
|
||||
|
||||
|
||||
@@ -71,7 +66,7 @@ def activate_user(user_id):
|
||||
user = User.from_id(user_id)
|
||||
|
||||
# TODO add org invites back in the new way
|
||||
# organization_id = redis_client.get(
|
||||
# organization_id = redis_client.raw_get(
|
||||
# f"organization-invite-{user.email_address}"
|
||||
# )
|
||||
# user_api_client.add_user_to_organization(
|
||||
@@ -83,7 +78,6 @@ def activate_user(user_id):
|
||||
return redirect(url_for("main.organization_dashboard", org_id=organization_id))
|
||||
else:
|
||||
activated_user = user.activate()
|
||||
current_app.logger.info(f"Activated user {user.id} #notify-admin-1505")
|
||||
activated_user.login()
|
||||
current_app.logger.info(f"Logged in user {user.id} #notify-admin-1505")
|
||||
|
||||
return redirect(url_for("main.add_service", first="first"))
|
||||
|
||||
@@ -390,7 +390,7 @@ class Service(JSONModel, SortByNameMixin):
|
||||
def get_data_retention_item(self, id):
|
||||
return next((dr for dr in self.data_retention if dr["id"] == id), None)
|
||||
|
||||
def get_days_of_retention(self, notification_type, number_of_days):
|
||||
def get_days_of_retention(self, notification_type):
|
||||
return next(
|
||||
(
|
||||
dr
|
||||
@@ -398,10 +398,7 @@ class Service(JSONModel, SortByNameMixin):
|
||||
if dr["notification_type"] == notification_type
|
||||
),
|
||||
{},
|
||||
).get(
|
||||
"days_of_retention",
|
||||
current_app.config["ACTIVITY_STATS_LIMIT_DAYS"].get(number_of_days),
|
||||
)
|
||||
).get("days_of_retention", current_app.config["ACTIVITY_STATS_LIMIT_DAYS"])
|
||||
|
||||
@cached_property
|
||||
def organization(self):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from flask import abort, current_app, request, session
|
||||
@@ -25,15 +24,11 @@ from app.utils.user_permissions import (
|
||||
|
||||
|
||||
def _get_service_id_from_view_args():
|
||||
if request and request.view_args:
|
||||
return str(request.view_args.get("service_id", ""))
|
||||
return None
|
||||
return str(request.view_args.get("service_id", "")) or None
|
||||
|
||||
|
||||
def _get_org_id_from_view_args():
|
||||
if request and request.view_args:
|
||||
return str(request.view_args.get("org_id", ""))
|
||||
return None
|
||||
return str(request.view_args.get("org_id", "")) or None
|
||||
|
||||
|
||||
class User(JSONModel, UserMixin):
|
||||
@@ -59,7 +54,6 @@ class User(JSONModel, UserMixin):
|
||||
super().__init__(_dict)
|
||||
self.permissions = _dict.get("permissions", {})
|
||||
self._platform_admin = _dict["platform_admin"]
|
||||
self.preferred_timezone = _dict.get("preferred_timezone", "America/New_York")
|
||||
|
||||
@classmethod
|
||||
def from_id(cls, user_id):
|
||||
@@ -153,13 +147,6 @@ class User(JSONModel, UserMixin):
|
||||
else:
|
||||
return self
|
||||
|
||||
def deactivate(self):
|
||||
if self.is_active:
|
||||
user_data = user_api_client.deactivate_user(self.id)
|
||||
return self.__class__(user_data["data"])
|
||||
else:
|
||||
return self
|
||||
|
||||
def login(self):
|
||||
login_user(self)
|
||||
session["user_id"] = self.id
|
||||
@@ -201,7 +188,7 @@ class User(JSONModel, UserMixin):
|
||||
@property
|
||||
def is_gov_user(self):
|
||||
is_gov = is_gov_user(self.email_address)
|
||||
# current_app.logger.info(f"User {self.id} is_gov_user: {is_gov}")
|
||||
current_app.logger.info(f"User {self.id} is_gov_user: {is_gov}")
|
||||
return is_gov
|
||||
|
||||
@property
|
||||
@@ -210,9 +197,9 @@ class User(JSONModel, UserMixin):
|
||||
|
||||
@property
|
||||
def platform_admin(self):
|
||||
# current_app.logger.warning(
|
||||
# f"Checking User {self.id} for platform admin: {self._platform_admin}"
|
||||
# )
|
||||
current_app.logger.warn(
|
||||
f"Checking User {self.id} for platform admin: {self._platform_admin}"
|
||||
)
|
||||
return self._platform_admin and not session.get(
|
||||
"disable_platform_admin_view", False
|
||||
)
|
||||
@@ -220,14 +207,6 @@ class User(JSONModel, UserMixin):
|
||||
def has_permissions(
|
||||
self, *permissions, restrict_admin_usage=False, allow_org_user=False
|
||||
):
|
||||
# TODO need this for load test, but breaks unit tests
|
||||
if self.platform_admin and os.getenv("NOTIFY_ENVIRONMENT") in (
|
||||
"development",
|
||||
"staging",
|
||||
"demo",
|
||||
):
|
||||
return True
|
||||
|
||||
unknown_permissions = set(permissions) - all_ui_permissions
|
||||
if unknown_permissions:
|
||||
raise TypeError(
|
||||
@@ -241,32 +220,25 @@ class User(JSONModel, UserMixin):
|
||||
if not service_id and not org_id:
|
||||
# we shouldn't have any pages that require permissions, but don't specify a service or organization.
|
||||
# use @user_is_platform_admin for platform admin only pages
|
||||
# raise NotImplementedError
|
||||
# current_app.logger.warning(f"VIEW ARGS ARE {request.view_args}")
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
log_msg = f"has_permissions user: {self.id} service: {service_id}"
|
||||
# platform admins should be able to do most things (except eg send messages, or create api keys)
|
||||
if self.platform_admin and not restrict_admin_usage:
|
||||
current_app.logger.debug(
|
||||
"has_permissions is true because user is platform_admin"
|
||||
)
|
||||
current_app.logger.warn(f"{log_msg} true because user is platform_admin")
|
||||
return True
|
||||
|
||||
if org_id:
|
||||
value = self.belongs_to_organization(org_id)
|
||||
current_app.logger.debug(
|
||||
f"has_permissions returns org: {org_id} returning {value}"
|
||||
)
|
||||
current_app.logger.warn(f"{log_msg} org: {org_id} returning {value}")
|
||||
return value
|
||||
|
||||
if not permissions and self.belongs_to_service(service_id):
|
||||
current_app.logger.debug("has_permissions True because belongs_to_service")
|
||||
current_app.logger.warn(f"{log_msg} True because belongs_to_service")
|
||||
return True
|
||||
|
||||
if any(self.permissions_for_service(service_id) & set(permissions)):
|
||||
current_app.logger.debug(
|
||||
"has_permissions returns True because permissions valid"
|
||||
)
|
||||
current_app.logger.warn(f"{log_msg} permissions valid")
|
||||
return True
|
||||
|
||||
from app.models.service import Service
|
||||
@@ -274,7 +246,7 @@ class User(JSONModel, UserMixin):
|
||||
org_value = allow_org_user and self.belongs_to_organization(
|
||||
Service.from_id(service_id).organization_id
|
||||
)
|
||||
current_app.logger.debug(f"has_permissions returning {org_value}")
|
||||
current_app.logger.warn(f"{log_msg} returning {org_value}")
|
||||
return org_value
|
||||
|
||||
def permissions_for_service(self, service_id):
|
||||
@@ -282,7 +254,7 @@ class User(JSONModel, UserMixin):
|
||||
|
||||
def has_permission_for_service(self, service_id, permission):
|
||||
has_permission = permission in self.permissions_for_service(service_id)
|
||||
current_app.logger.debug(
|
||||
current_app.logger.warn(
|
||||
f"has_permission_for_service user: {self.id} service: {service_id} "
|
||||
f"permission: {permission} retuning {has_permission}"
|
||||
)
|
||||
@@ -563,17 +535,17 @@ class InvitedUser(JSONModel):
|
||||
return cls.by_id(invited_user_id) if invited_user_id else None
|
||||
|
||||
def has_permissions(self, *permissions):
|
||||
# current_app.logger.warning(
|
||||
# f"Checking invited user {self.id} for permissions: {permissions}"
|
||||
# )
|
||||
current_app.logger.warn(
|
||||
f"Checking invited user {self.id} for permissions: {permissions}"
|
||||
)
|
||||
if self.status == "cancelled":
|
||||
return False
|
||||
return set(self.permissions) > set(permissions)
|
||||
|
||||
def has_permission_for_service(self, service_id, permission):
|
||||
# current_app.logger.warn(
|
||||
# f"Checking invited user {self.id} for permission: {permission} on service {service_id}"
|
||||
# )
|
||||
current_app.logger.warn(
|
||||
f"Checking invited user {self.id} for permission: {permission} on service {service_id}"
|
||||
)
|
||||
if self.status == "cancelled":
|
||||
return False
|
||||
return self.service == service_id and permission in self.permissions
|
||||
|
||||
@@ -46,15 +46,6 @@ class HeaderNavigation(Navigation):
|
||||
"roadmap",
|
||||
"security",
|
||||
},
|
||||
"best_practices": {
|
||||
"best_practices",
|
||||
"clear_goals",
|
||||
"rules_and_regulations",
|
||||
"establish_trust",
|
||||
"write_for_action",
|
||||
"multiple_languages",
|
||||
"benchmark_performance"
|
||||
},
|
||||
"using_notify": {
|
||||
"get_started",
|
||||
"using_notify",
|
||||
@@ -132,7 +123,6 @@ class HeaderNavigation(Navigation):
|
||||
"get_billing_report",
|
||||
"get_users_report",
|
||||
"get_daily_volumes",
|
||||
"download_all_users",
|
||||
"get_daily_sms_provider_volumes",
|
||||
"get_volumes_by_service",
|
||||
"organizations",
|
||||
@@ -163,9 +153,6 @@ class HeaderNavigation(Navigation):
|
||||
|
||||
class MainNavigation(Navigation):
|
||||
mapping = {
|
||||
"activity": {
|
||||
"all_jobs_activity",
|
||||
},
|
||||
"dashboard": {
|
||||
"conversation",
|
||||
"inbox",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import os
|
||||
|
||||
from flask import abort, current_app, has_request_context, request
|
||||
from flask import abort, has_request_context, request
|
||||
from flask_login import current_user
|
||||
from notifications_python_client import __version__
|
||||
from notifications_python_client.base import BaseAPIClient
|
||||
@@ -56,47 +54,16 @@ class NotifyAdminAPIClient(BaseAPIClient):
|
||||
):
|
||||
abort(403)
|
||||
|
||||
def is_calling_signin_url(self, arg):
|
||||
return arg.startswith("('/user")
|
||||
|
||||
def check_inactive_user(self, *args):
|
||||
still_signing_in = False
|
||||
|
||||
# TODO clean up and add testing etc.
|
||||
# We really should be checking for exact matches
|
||||
# and we only want to check the first arg
|
||||
for arg in args:
|
||||
arg = str(arg)
|
||||
if self.is_calling_signin_url(arg):
|
||||
still_signing_in = True
|
||||
|
||||
# This seems to be a weird edge case that happens intermittently with invites
|
||||
if str(arg) == "()":
|
||||
still_signing_in = True
|
||||
# TODO: Update this once E2E tests are managed by a feature flag or some other main config option.
|
||||
if os.getenv("NOTIFY_E2E_TEST_EMAIL"):
|
||||
# allow end-to-end tests to skip check
|
||||
pass
|
||||
elif still_signing_in is True:
|
||||
# we are not full signed in yet
|
||||
pass
|
||||
elif not current_user or not current_user.is_active:
|
||||
current_app.logger.error(f"Unauthorized URL #notify-compliance-46 {args}")
|
||||
abort(403)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
self.check_inactive_service()
|
||||
self.check_inactive_user(args)
|
||||
return super().post(*args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
self.check_inactive_service()
|
||||
self.check_inactive_user()
|
||||
return super().put(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.check_inactive_service()
|
||||
self.check_inactive_user()
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import json
|
||||
import secrets
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import current_app, request
|
||||
|
||||
from app import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
from app.utils.user_permissions import (
|
||||
all_ui_permissions,
|
||||
translate_permissions_from_ui_to_db,
|
||||
)
|
||||
from notifications_utils.url_safe_token import generate_token
|
||||
|
||||
|
||||
class InviteApiClient(NotifyAdminAPIClient):
|
||||
@@ -40,35 +32,8 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
"folder_permissions": folder_permissions,
|
||||
}
|
||||
data = _attach_current_user(data)
|
||||
|
||||
ttl = 24 * 60 * 60
|
||||
|
||||
# make and store the state
|
||||
state = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state, ex=ttl)
|
||||
|
||||
# make and store the nonce
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce, ex=ttl) # save the nonce to redis.
|
||||
|
||||
data["nonce"] = nonce # This is passed to api for the invite url.
|
||||
data["state"] = state # This is passed to api for the invite url.
|
||||
|
||||
resp = self.post(url=f"/service/{service_id}/invite", data=data)
|
||||
|
||||
resp_data = resp["data"]
|
||||
invite_data_key = f"invitedata-{unquote(state)}"
|
||||
redis_invite_data = resp["invite"]
|
||||
redis_invite_data = json.dumps(redis_invite_data)
|
||||
redis_client.set(invite_data_key, redis_invite_data, ex=ttl)
|
||||
|
||||
return resp_data
|
||||
return resp["data"]
|
||||
|
||||
def get_invites_for_service(self, service_id):
|
||||
return self.get(f"/service/{service_id}/invite")["data"]
|
||||
@@ -99,34 +64,7 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
|
||||
|
||||
def resend_invite(self, service_id, invited_user_id):
|
||||
ttl = 24 * 60 * 60
|
||||
|
||||
# make and store the state
|
||||
state = generate_token(
|
||||
str(request.remote_addr),
|
||||
current_app.config["SECRET_KEY"],
|
||||
current_app.config["DANGEROUS_SALT"],
|
||||
)
|
||||
state_key = f"login-state-{unquote(state)}"
|
||||
redis_client.set(state_key, state, ex=ttl)
|
||||
|
||||
# make and store the nonce
|
||||
nonce = secrets.token_urlsafe()
|
||||
nonce_key = f"login-nonce-{unquote(nonce)}"
|
||||
redis_client.set(nonce_key, nonce, ex=ttl)
|
||||
|
||||
data = {
|
||||
"nonce": nonce,
|
||||
"state": state,
|
||||
}
|
||||
resp = self.post(
|
||||
url=f"/service/{service_id}/invite/{invited_user_id}/resend", data=data
|
||||
)
|
||||
|
||||
invite_data_key = f"invitedata-{unquote(state)}"
|
||||
redis_invite_data = resp["invite"]
|
||||
redis_invite_data = json.dumps(redis_invite_data)
|
||||
redis_client.set(invite_data_key, redis_invite_data, ex=ttl)
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}/resend", data={})
|
||||
|
||||
@cache.delete("service-{service_id}")
|
||||
@cache.delete("user-{invited_user_id}")
|
||||
|
||||
@@ -27,7 +27,9 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
|
||||
def get_job(self, service_id, job_id):
|
||||
params = {}
|
||||
job = self.get(url=f"/service/{service_id}/job/{job_id}", params=params)
|
||||
job = self.get(
|
||||
url="/service/{}/job/{}".format(service_id, job_id), params=params
|
||||
)
|
||||
|
||||
return job
|
||||
|
||||
@@ -38,14 +40,13 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
if statuses is not None:
|
||||
params["statuses"] = ",".join(statuses)
|
||||
|
||||
job = self.get(url=f"/service/{service_id}/job", params=params)
|
||||
return job
|
||||
return self.get(url="/service/{}/job".format(service_id), params=params)
|
||||
|
||||
def get_uploads(self, service_id, limit_days=None, page=1):
|
||||
params = {"page": page}
|
||||
if limit_days is not None:
|
||||
params["limit_days"] = limit_days
|
||||
return self.get(url=f"/service/{service_id}/upload", params=params)
|
||||
return self.get(url="/service/{}/upload".format(service_id), params=params)
|
||||
|
||||
def has_sent_previously(
|
||||
self, service_id, template_id, template_version, original_file_name
|
||||
@@ -146,5 +147,10 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
url="/service/{}/job/{}/cancel".format(service_id, job_id), data={}
|
||||
)
|
||||
|
||||
def start_job(self, service_id, job_id):
|
||||
return self.post(
|
||||
url="/service/{}/job/{}/start-job".format(service_id, job_id), data={}
|
||||
)
|
||||
|
||||
|
||||
job_api_client = JobApiClient()
|
||||
|
||||
@@ -37,11 +37,10 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
||||
# we do not want in our logs, so we do a POST request instead of a GET
|
||||
method = self.post if to else self.get
|
||||
kwargs = {"data": params} if to else {"params": params}
|
||||
|
||||
if job_id:
|
||||
return method(
|
||||
url="/service/{}/job/{}/notifications".format(service_id, job_id),
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
if limit_days is not None:
|
||||
@@ -100,5 +99,8 @@ class NotificationApiClient(NotifyAdminAPIClient):
|
||||
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
|
||||
)["count"]
|
||||
|
||||
def get_total_notification_message_parts_by_job_id(self, job_id):
|
||||
return self.get(url="/notifications/{}/message_parts".format(job_id))
|
||||
|
||||
|
||||
notification_api_client = NotificationApiClient()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
@@ -43,28 +43,6 @@ class ServiceAPIClient(NotifyAdminAPIClient):
|
||||
params={"limit_days": limit_days},
|
||||
)["data"]
|
||||
|
||||
def get_service_notification_statistics_by_day(
|
||||
self, service_id, start_date=None, days=None
|
||||
):
|
||||
if start_date is None:
|
||||
start_date = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
return self.get(
|
||||
"/service/{0}/statistics/{1}/{2}".format(service_id, start_date, days),
|
||||
)["data"]
|
||||
|
||||
def get_user_service_notification_statistics_by_day(
|
||||
self, service_id, user_id, start_date=None, days=None
|
||||
):
|
||||
if start_date is None:
|
||||
start_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
return self.get(
|
||||
"/service/{0}/statistics/user/{1}/{2}/{3}".format(
|
||||
service_id, user_id, start_date, days
|
||||
),
|
||||
)["data"]
|
||||
|
||||
def get_services(self, params_dict=None):
|
||||
"""
|
||||
Retrieve a list of services.
|
||||
|
||||
@@ -2,7 +2,6 @@ from flask import current_app
|
||||
from notifications_python_client.errors import HTTPError
|
||||
|
||||
from app.notify_client import NotifyAdminAPIClient, cache
|
||||
from app.utils import hilite
|
||||
from app.utils.user_permissions import translate_permissions_from_ui_to_db
|
||||
|
||||
ALLOWED_ATTRIBUTES = {
|
||||
@@ -110,14 +109,13 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
raise
|
||||
|
||||
def send_verify_code(self, user_id, code_type, to, next_string=None):
|
||||
|
||||
data = {"to": to}
|
||||
if next_string:
|
||||
data["next"] = next_string
|
||||
if code_type == "email":
|
||||
data["email_auth_link_host"] = self.admin_url
|
||||
endpoint = f"/user/{user_id}/{code_type}-code"
|
||||
current_app.logger.warn(hilite(f"Sending verify_code {code_type} to {user_id}"))
|
||||
endpoint = "/user/{0}/{1}-code".format(user_id, code_type)
|
||||
current_app.logger.warn(f"Sending verify_code {code_type} to {user_id}")
|
||||
self.post(endpoint, data=data)
|
||||
|
||||
def send_verify_email(self, user_id, to):
|
||||
@@ -159,16 +157,12 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
endpoint = "/user"
|
||||
return self.get(endpoint)["data"]
|
||||
|
||||
def get_all_users_detailed(self):
|
||||
endpoint = "/user/report-all-users"
|
||||
return self.get(endpoint)["data"]
|
||||
|
||||
@cache.delete("service-{service_id}")
|
||||
@cache.delete("service-{service_id}-template-folders")
|
||||
@cache.delete("user-{user_id}")
|
||||
def add_user_to_service(self, service_id, user_id, permissions, folder_permissions):
|
||||
# permissions passed in are the combined UI permissions, not DB permissions
|
||||
endpoint = f"/service/{service_id}/users/{user_id}"
|
||||
endpoint = "/service/{}/users/{}".format(service_id, user_id)
|
||||
data = {
|
||||
"permissions": [
|
||||
{"permission": x}
|
||||
@@ -223,10 +217,6 @@ class UserApiClient(NotifyAdminAPIClient):
|
||||
def activate_user(self, user_id):
|
||||
return self.post("/user/{}/activate".format(user_id), data=None)
|
||||
|
||||
@cache.delete("user-{user_id}")
|
||||
def deactivate_user(self, user_id):
|
||||
return self.post("/user/{}/deactivate".format(user_id), data=None)
|
||||
|
||||
def send_change_email_verification(self, user_id, new_email):
|
||||
endpoint = "/user/{}/change-email-verification".format(user_id)
|
||||
data = {"email": new_email}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import os
|
||||
|
||||
import botocore
|
||||
from boto3 import Session
|
||||
from botocore.config import Config
|
||||
@@ -29,22 +27,8 @@ def get_s3_object(
|
||||
aws_secret_access_key=secret_key,
|
||||
region_name=region,
|
||||
)
|
||||
s3 = session.resource(
|
||||
"s3",
|
||||
config=AWS_CLIENT_CONFIG,
|
||||
)
|
||||
s3 = session.resource("s3", config=AWS_CLIENT_CONFIG)
|
||||
obj = s3.Object(bucket_name, filename)
|
||||
# This 'proves' that use of moto in the relevant tests in test_send.py
|
||||
# mocks everything related to S3. What you will see in the logs is:
|
||||
# Exception: CREATED AT <MagicMock name='resource().Bucket().creation_date' id='4665562448'>
|
||||
#
|
||||
# raise Exception(f"CREATED AT {_s3.Bucket(bucket_name).creation_date}")
|
||||
if os.getenv("NOTIFY_ENVIRONMENT") == "test":
|
||||
teststr = str(s3.Bucket(bucket_name).creation_date).lower()
|
||||
if "magicmock" not in teststr:
|
||||
raise Exception(
|
||||
"Test is not mocked, use @mock_aws or the relevant mocker.patch to avoid accessing S3"
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from flask import current_app
|
||||
@@ -11,13 +10,13 @@ from app.s3_client import (
|
||||
)
|
||||
from notifications_utils.s3 import s3upload as utils_s3upload
|
||||
|
||||
NEW_FILE_LOCATION_STRUCTURE = "{}-service-notify/{}.csv"
|
||||
FILE_LOCATION_STRUCTURE = "service-{}-notify/{}.csv"
|
||||
|
||||
|
||||
def get_csv_location(service_id, upload_id):
|
||||
return (
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["bucket"],
|
||||
NEW_FILE_LOCATION_STRUCTURE.format(service_id, upload_id),
|
||||
FILE_LOCATION_STRUCTURE.format(service_id, upload_id),
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["access_key_id"],
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["secret_access_key"],
|
||||
current_app.config["CSV_UPLOAD_BUCKET"]["region"],
|
||||
@@ -29,19 +28,10 @@ def get_csv_upload(service_id, upload_id):
|
||||
|
||||
|
||||
def s3upload(service_id, filedata):
|
||||
|
||||
upload_id = str(uuid.uuid4())
|
||||
bucket_name, file_location, access_key, secret_key, region = get_csv_location(
|
||||
service_id, upload_id
|
||||
)
|
||||
if bucket_name == "":
|
||||
exp_bucket = current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
|
||||
exp_region = current_app.config["CSV_UPLOAD_BUCKET"]["region"]
|
||||
tier = os.getenv("NOTIFY_ENVIRONMENT")
|
||||
raise Exception(
|
||||
f"NO BUCKET NAME SHOULD BE: {exp_bucket} WITH REGION {exp_region} TIER {tier}"
|
||||
)
|
||||
|
||||
utils_s3upload(
|
||||
filedata=filedata["data"],
|
||||
region=region,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% block per_page_title %}{% endblock %} – Notify.gov
|
||||
{% endblock %}
|
||||
</title>
|
||||
{% include "components/head.html" %}
|
||||
{% include "new/components/head.html" %}
|
||||
</head>
|
||||
<body class="usa-template__body {{ bodyClasses }}">
|
||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||
@@ -32,8 +32,8 @@
|
||||
|
||||
|
||||
{% block header %}
|
||||
{% include 'components/usa_banner.html' %}
|
||||
{% include 'components/header.html' %}
|
||||
{% include 'new/components/usa_banner.html' %}
|
||||
{% include 'new/components/header.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
@@ -47,36 +47,15 @@
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% block flash_messages %}
|
||||
{% include 'components/flash_messages.html' %}
|
||||
{% include 'flash_messages.html' %}
|
||||
{% endblock %}
|
||||
{% block maincolumn_content %}
|
||||
<div class="grid-row">
|
||||
{% if navigation_links %}
|
||||
<div class="tablet:grid-col-2 margin-bottom-4">
|
||||
<nav class="nav">
|
||||
<ul class="usa-sidenav">
|
||||
{% for item in navigation_links %}
|
||||
<li class="usa-sidenav__item">
|
||||
<a href="{{ url_for(item.link) }}"
|
||||
class="parent-link {% if item['link'] == request.endpoint %} usa-current {% endif %}"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
{% if item.sub_navigation_items %}
|
||||
<ul class="usa-sidenav__sublist" role="menu">
|
||||
{% for sub_item in item.sub_navigation_items %}
|
||||
<li role="menuitem">
|
||||
<a href="{{ url_for(sub_item.link.split('#')[0]) }}#{{ sub_item.link.split('#')[1] }}">{{ sub_item.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{{ sub_navigation(navigation_links) }}
|
||||
</div>
|
||||
<div class="tablet:grid-col-10 tablet:padding-left-4 usa-prose site-prose">
|
||||
<div class="tablet:grid-col-10 padding-left-4 usa-prose site-prose">
|
||||
{% else %}
|
||||
<div class="tablet:grid-col-10">
|
||||
{% endif %}
|
||||
@@ -99,11 +78,11 @@
|
||||
{% set meta_suffix = "Built by the " + long_link + commit_hash %}
|
||||
{% endif %}
|
||||
|
||||
{% include "components/footer.html" %}
|
||||
{% include "new/components/footer.html" %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
{% block sessionUserWarning %}
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timeLeft">
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timerWarning">
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="sessionTimerHeading">
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<ul class="usa-card-group">
|
||||
{% for item in card_contents %}
|
||||
<li class="usa-card grid-col tablet:grid-col-4 {% if item.link %}linked-card linked-content{% endif %}">
|
||||
<div class="usa-card__container">
|
||||
{% if item.card_heading %}
|
||||
<div class="usa-card__header">
|
||||
{% if item.link %}
|
||||
<a href="{{ item.link }}">
|
||||
{% endif %}
|
||||
<h3
|
||||
class="usa-card__heading {% if text_align != 'left' or text_align is not defined %}text-center{% else %}text-left{% endif %} {% if item.link %}linked-card{% endif %}">
|
||||
{{ item.card_heading }}
|
||||
</h3>
|
||||
{% if item.link %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.svg_src or item.image_src%}
|
||||
<div class="usa-card__media usa-card__media--inset">
|
||||
{% if item.svg_src %}
|
||||
<svg aria-hidden="true" focusable="false" role="img" class="best-practices_card_img">
|
||||
<use xlink:href="{{asset_url('images/best-practices-card-sprite.svg')}}#{{item.svg_src}}"></use>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{% if item.image_src %}
|
||||
<img class="best-practices_card_img" src="{{item.image_src}}" alt="{{ item.alt_text }}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.p_text %}
|
||||
<div class="usa-card__body">
|
||||
<p
|
||||
class="{% if text_align != 'left' or text_align is not defined %}text-center{% else %}text-left{% endif %} {% if item.link %}linked-card{% endif %}">
|
||||
{{item.p_text|safe}}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -1,9 +0,0 @@
|
||||
{% macro circle_number(number) %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="svg-circle-icon" width="24" height="24" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="37" stroke="#005ea2" stroke-width="7" fill="none" />
|
||||
<text x="50%" y="50%" text-anchor="middle" font-size="40" font-weight="bolder" fill="#005ea2" font-family="Arial"
|
||||
dy=".35em">
|
||||
{{ number }}
|
||||
</text>
|
||||
</svg>
|
||||
{% endmacro %}
|
||||
@@ -1,13 +0,0 @@
|
||||
<nav class="navigation-service usa-breadcrumb padding-top-0">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<span class="usa-breadcrumb__label"><a href="{{ url_for('.best_practices') }}"
|
||||
class="usa-link navigation-organization-link">Best Practices</a></span>
|
||||
</li>
|
||||
{% if title %}
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<span class="usa-breadcrumb__label">{{ title }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
@@ -1,4 +1,2 @@
|
||||
<nav class="usa-breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="{{ params.href or '#' }}" class="usa-link usa-back-link display-inline-flex {{ params.classes or '' }}"
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {% endfor %}>{{ params.html | safe or params.text or 'Back' }}</a>
|
||||
</nav>
|
||||
<a href="{%- if params.href %}{{ params.href }}{% else %}#{% endif -%}" class="usa-link usa-back-link display-inline-flex margin-bottom-3 {%- if params.classes %} {{ params.classes }}{% endif -%}"
|
||||
{%- for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}>{{ (params.html | safe if params.html else (params.text if params.text else 'Back')) }}</a>
|
||||
|
||||
15
app/templates/components/components/footer/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Footer
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the footer component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/footer).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
|
||||
See [options table](https://design-system.service.gov.uk/components/footer/#options-example-default) for details.
|
||||
115
app/templates/components/components/footer/macro-options.json
Normal file
@@ -0,0 +1,115 @@
|
||||
[
|
||||
{
|
||||
"name": "meta",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "Object containing options for the meta navigation.",
|
||||
"params": [
|
||||
{
|
||||
"name": "visuallyHiddenTitle",
|
||||
"type": "string",
|
||||
"description": "Title for a meta item section, which defaults to Support links"
|
||||
},
|
||||
{
|
||||
"name": "html",
|
||||
"type": "string",
|
||||
"description": "HTML to add to the meta section of the footer, which will appear below any links specified using meta.items."
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"description": "Text to add to the meta section of the footer, which will appear below any links specified using meta.items. If meta.html is specified, this option is ignored."
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Array of items for use in the meta section of the footer.",
|
||||
"params": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item text in the meta section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "href",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item href attribute in the meta section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the anchor in the footer meta section."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "navigation",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Array of items for use in the navigation section of the footer.",
|
||||
"params": [
|
||||
{
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Title for a section"
|
||||
},
|
||||
{
|
||||
"name": "columns",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"description": "Amount of columns to display items in navigation section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "items",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "Array of items to display in the list in navigation section of the footer.",
|
||||
"params": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item text in the navigation section of the footer."
|
||||
},
|
||||
{
|
||||
"name": "href",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "List item href attribute in the navigation section of the footer. Both `text` and `href` attributes need to be present to create a link."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the anchor in the footer navigation section."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "containerClasses",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes that can be added to the inner container, useful if you want to make the footer full width."
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the footer component container."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the footer component container."
|
||||
}
|
||||
]
|
||||
3
app/templates/components/components/footer/macro.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro usaFooter(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
85
app/templates/components/components/footer/template.njk
Normal file
@@ -0,0 +1,85 @@
|
||||
<footer class="usa-footer usa-footer--big">
|
||||
{# <div class="grid-container usa-footer__return-to-top">
|
||||
<a href="#">Return to top</a>
|
||||
</div> #}
|
||||
<div class="usa-footer__primary-section">
|
||||
<div class="grid-container">
|
||||
<div class="padding-y-1">
|
||||
{% if params.meta %}
|
||||
<h2 class="usa-sr-only">{{ params.meta.visuallyHiddenTitle | default("Support links") }}</h2>
|
||||
{% if params.meta.items %}
|
||||
{% for item in params.meta.items %}
|
||||
<a class="usa-footer__link" href="{{ item.href }}" {% for attribute, value in item.attributes %}
|
||||
{{attribute}}="{{value}}" {% endfor %}>
|
||||
{{ item.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if params.meta.text or params.meta.html %}
|
||||
<div>
|
||||
{{ params.meta.html | safe if params.meta.html else params.meta.text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="usa-identifier site-identifier">
|
||||
<section class="usa-identifier__section usa-identifier__section--masthead" aria-label="Agency identifier,,,,,,">
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__logos"><a href="javascript:void(0);" class="usa-identifier__logo">
|
||||
<img class="usa-identifier__logo-img" src="{{ params.assetsPath | default('/static/images') }}/gsa-logo.svg" alt="GSA Logo" role="img" width="48"
|
||||
height="48">
|
||||
</a></div>
|
||||
<section class="usa-identifier__identity" aria-label="Agency description">
|
||||
<p class="usa-identifier__identity-domain">beta.notify.gov</p>
|
||||
<p class="usa-identifier__identity-disclaimer">An official website of the <a href="https://gsa.gov">General
|
||||
Services Administration</a></p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="usa-identifier__section usa-identifier__section--required-links" aria-label="Important links">
|
||||
<div class="usa-identifier__container">
|
||||
<ul class="usa-identifier__required-links-list">
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/about-us" class="usa-identifier__required-link">About GSA</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/website-information/accessibility-aids"
|
||||
class="usa-identifier__required-link">Accessibility support</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/freedom-of-information-act-foia"
|
||||
class="usa-identifier__required-link">FOIA requests</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/civil-rights-programs/notification-and-federal-employee-antidiscrimination-and-retaliation-act-of-2002"
|
||||
class="usa-identifier__required-link">No FEAR Act data</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsaig.gov/" class="usa-identifier__required-link">Office of the Inspector General</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/reference/reports/budget-performance"
|
||||
class="usa-identifier__required-link">Performance reports</a>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="https://www.gsa.gov/website-information/website-policies"
|
||||
class="usa-identifier__required-link">Privacy policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="usa-identifier__section usa-identifier__section--usagov"
|
||||
aria-label="Government information and services">
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__usagov-description">
|
||||
Looking for U.S. government information and services?
|
||||
</div>
|
||||
<a href="https://www.usa.gov/" class="usa-link">Visit USA.gov</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
15
app/templates/components/components/header/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Header
|
||||
|
||||
## Installation
|
||||
|
||||
See the [main README quick start guide](https://github.com/alphagov/govuk-frontend#quick-start) for how to install this component.
|
||||
|
||||
## Guidance and Examples
|
||||
|
||||
Find out when to use the header component in your service in the [GOV.UK Design System](https://design-system.service.gov.uk/components/header).
|
||||
|
||||
## Component options
|
||||
|
||||
Use options to customize the appearance, content and behavior of a component when using a macro, for example, changing the text.
|
||||
|
||||
See [options table](https://design-system.service.gov.uk/components/header/#options-example-default) for details.
|
||||
@@ -0,0 +1,88 @@
|
||||
[
|
||||
{
|
||||
"name": "homepageUrl",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The url of the homepage. Defaults to /"
|
||||
},
|
||||
{
|
||||
"name": "assetsPath",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "The public path for the assets folder. If not provided it defaults to /assets/images"
|
||||
},
|
||||
{
|
||||
"name": "productName",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Header title that is placed next to GOV.UK. Used for product names (i.e. Pay, Verify)"
|
||||
},
|
||||
{
|
||||
"name": "serviceName",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Header title that is placed next to GOV.UK. Used for product names (i.e. Pay, Verify)"
|
||||
},
|
||||
{
|
||||
"name": "serviceUrl",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Url for the service name anchor."
|
||||
},
|
||||
{
|
||||
"name": "navigation",
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"description": "An array of navigation item objects.",
|
||||
"params": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Text of the navigation item."
|
||||
},
|
||||
{
|
||||
"name": "href",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Url of the navigation item anchor. Both `href` and `text` attributes for navigation items need to be provided to create an item."
|
||||
},
|
||||
{
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"description": "Flag to mark the navigation item as active or not."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the navigation item anchor."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "navigationClasses",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes for the navigation section of the header."
|
||||
},
|
||||
{
|
||||
"name": "containerClasses",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes for the container, useful if you want to make the header fixed width."
|
||||
},
|
||||
{
|
||||
"name": "classes",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"description": "Classes to add to the header container."
|
||||
},
|
||||
{
|
||||
"name": "attributes",
|
||||
"type": "object",
|
||||
"required": false,
|
||||
"description": "HTML attributes (for example data attributes) to add to the header container."
|
||||
}
|
||||
]
|
||||
3
app/templates/components/components/header/macro.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro usaHeader(params) %}
|
||||
{%- include "./template.njk" -%}
|
||||
{% endmacro %}
|
||||
105
app/templates/components/components/header/template.njk
Normal file
@@ -0,0 +1,105 @@
|
||||
<!-- usa banner -->
|
||||
<section class="usa-banner site-banner" aria-label="Official website of the United States government,,,,,,">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
<div class="usa-banner__inner">
|
||||
<div class="grid-col-auto">
|
||||
<img aria-hidden="true" class="usa-banner__header-flag" src="/static/img/us_flag_small.png" alt="" />
|
||||
</div>
|
||||
<div class="grid-col-fill tablet:grid-col-auto" aria-hidden="true">
|
||||
<p class="usa-banner__header-text">An official website of the United States government</p>
|
||||
<p class="usa-banner__header-action">Here’s how you know</p>
|
||||
</div>
|
||||
<button type="button" class="usa-accordion__button usa-banner__button" aria-expanded="false" aria-controls="gov-banner">
|
||||
<span class="usa-banner__button-text">Here’s how you know</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="usa-banner__content usa-accordion__content" id="gov-banner" hidden="">
|
||||
<div class="grid-row grid-gap-lg">
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-dot-gov.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Official websites use .gov</strong>
|
||||
<br>
|
||||
A <strong>.gov</strong> website belongs to an official government organization in the United States.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usa-banner__guidance tablet:grid-col-6">
|
||||
<img class="usa-banner__icon usa-media-block__img" src="/static/img/icon-https.svg" role="img" alt="" aria-hidden="true">
|
||||
<div class="usa-media-block__body">
|
||||
<p>
|
||||
<strong>Secure .gov websites use HTTPS</strong>
|
||||
<br>
|
||||
A <strong>lock</strong> ( <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" width="52" height="64" viewBox="0 0 52 64" class="usa-banner__lock-image" role="img" aria-labelledby="banner-lock-description" focusable="false"><title id="banner-lock-title">Lock</title><desc id="banner-lock-description">Locked padlock</desc><path fill="#000000" fill-rule="evenodd" d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"></path></svg></span> ) or <strong>https://</strong> means you’ve safely connected to the .gov website. Share sensitive information only on official, secure websites.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- usa header -->
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<image src="{{ params.assetsPath | default('/static/images') }}/notify-logo.png" alt="Notify.gov logo" xlink:href=""
|
||||
class="usa-flag-logo margin-right-1"></image>
|
||||
</a>
|
||||
</div>
|
||||
{% if params.navigation %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in params.navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% for item in params.secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- <section aria-label="Search component">
|
||||
<form class="usa-search usa-search--small margin-bottom-2" role="search">
|
||||
<label class="usa-sr-only" for="search-field">Search</label>
|
||||
<input class="usa-input" id="search-field" type="search" name="search" />
|
||||
<button class="usa-button" type="submit">
|
||||
<img src="/static/images/usa-icons-bg/search--white.svg" class="usa-search__submit-icon" alt="Search" />
|
||||
</button>
|
||||
</form>
|
||||
</section> -->
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,3 +1,7 @@
|
||||
<a href="{{ params.href | default('#content') }}" class="usa-skipnav{%- if params.classes %} {{ params.classes }}{% endif -%}"{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}>
|
||||
{{- params.html | safe if params.html else params.text -}}
|
||||
</a>
|
||||
|
||||
<a class="usa-skipnav " href="#main-content">
|
||||
Skip to main content
|
||||
</a>
|
||||
@@ -1,95 +0,0 @@
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active":
|
||||
header_navigation.is_selected('accounts-or-dashboard')},
|
||||
{"href": url_for('main.get_started'), "text": "Using Notify", "active": header_navigation.is_selected('using_notify')}
|
||||
] %}
|
||||
|
||||
{% if FEATURE_BEST_PRACTICES_ENABLED %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.best_practices'), "text": "Guides", "active":
|
||||
header_navigation.is_selected('best_practices')}] %}
|
||||
{% endif %}
|
||||
|
||||
{% set navigation = navigation + [
|
||||
{"href": url_for('main.features'), "text": "Features", "active": header_navigation.is_selected('features')},
|
||||
{"href": url_for('main.support'), "text": "Contact us", "active": header_navigation.is_selected('support')}
|
||||
] %}
|
||||
|
||||
{% if current_user.platform_admin %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin",
|
||||
"active": header_navigation.is_selected('platform-admin')}] %}
|
||||
{% else %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active":
|
||||
header_navigation.is_selected('user-profile')}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_service %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active":
|
||||
secondary_navigation.is_selected('settings')},
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.svg' }}" alt="Notify.gov logo"
|
||||
class="usa-flag-logo margin-right-1">
|
||||
</a>
|
||||
</div>
|
||||
{% if navigation %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="{{ asset_url('img/usa-icons/close.svg') }}" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value
|
||||
in item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% if secondaryNavigation %}
|
||||
{% for item in secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute,
|
||||
value in item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -6,7 +6,7 @@
|
||||
<thead class="table-field-headings{% if field_headings_visible %}-visible{% endif %}">
|
||||
<tr>
|
||||
{% for field_heading in field_headings %}
|
||||
<th class="table-field-heading{% if loop.first %}-first{% endif %}" width="{% if equal_length %}{{ (100 / field_headings|length)|int }}%{% endif %}">
|
||||
<th scope="col" class="table-field-heading{% if loop.first %}-first{% endif %}" width="{% if equal_length %}{{ (100 / field_headings|length)|int }}%{% endif %}">
|
||||
{% if field_headings_visible %}
|
||||
{{ field_heading }}
|
||||
{% else %}
|
||||
@@ -79,9 +79,9 @@
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro row_heading() -%}
|
||||
<td>
|
||||
<th class="table-field">
|
||||
{{ caller() }}
|
||||
</td>
|
||||
</th>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro index_field(text=None, rowspan=None) -%}
|
||||
|
||||
31
app/templates/main_nav.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% if help %}
|
||||
{% include 'partials/tour.html' %}
|
||||
{% else %}
|
||||
<nav class="nav margin-bottom-4">
|
||||
<a class="usa-button margin-top-1 margin-bottom-5 width-full"
|
||||
href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a>
|
||||
<ul class="usa-sidenav">
|
||||
{% if current_user.has_permissions() %}
|
||||
{% if current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
|
||||
{% endif %}
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('templates') }}" href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a></li> #}
|
||||
{% if not current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
|
||||
{% endif %}
|
||||
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||
{% endif %} -->
|
||||
{% if current_user.has_permissions('manage_api_keys') %}
|
||||
<!-- <li><a class="usa-link{{ main_navigation.is_selected('api-integration') }}" href="{{ url_for('.api_integration', service_id=current_service.id) }}">API integration</a></li> -->
|
||||
{% endif %}
|
||||
{% elif current_user.has_permissions(allow_org_user=True) %}
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
@@ -58,20 +58,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="usa-identifier__section usa-identifier__section--usagov"
|
||||
aria-label="Github Repos">
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__required-links-item">
|
||||
Find us on Github:
|
||||
</div>
|
||||
<ul>
|
||||
<li><a href="https://github.com/gsa/notifications-admin" class="usa-identifier__required-link">Notify.gov Admin repo</a></li>
|
||||
<li><a href="https://github.com/gsa/notifications-api" class="usa-identifier__required-link">Notify.gov API repo</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="usa-identifier__section usa-identifier__section--usagov"
|
||||
aria-label="Government information and services">
|
||||
<div class="usa-identifier__container">
|
||||
@@ -11,16 +11,17 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
|
||||
<link rel="shortcut icon" href="{{ assetPath | default('/static') }}/images/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ assetPath | default('/static') }}/images/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/static') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/static') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/static') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="{{ asset_url('images/site.webmanifest') }}">
|
||||
<link rel="shortcut icon" href="{{ assetPath | default('/assets') }}/images/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ assetPath | default('/assets') }}/images/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ assetPath | default('/assets') }}/images/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="{{ assetPath | default('/assets') }}/images/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ assetPath | default('/assets') }}/images/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<link href="{{ assetPath | default('/static') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<link href="{{ assetPath | default('/assets') }}/images/notify-dark-favicon.png" rel="icon" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
|
||||
<link rel="stylesheet" media="screen" href="{{ asset_url('css/styles.css') }}" />
|
||||
{% block extra_stylesheets %}{% endblock %}
|
||||
|
||||
@@ -30,7 +31,8 @@
|
||||
<meta property="og:image" content="/static/images/notify-og-image.png">
|
||||
{# google #}
|
||||
<script type="text/javascript" src="{{ asset_url('js/gtm_head.js') }}"></script>
|
||||
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" /> {# pragma: allowlist secret #}
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/chart.umd.js') }}"></script>
|
||||
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
|
||||
{% if g.hide_from_search_engines %}
|
||||
<meta name="robots" content="noindex" />
|
||||
{% endif %}
|
||||
79
app/templates/new/components/header.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{# setting navigation and secondarynavigation #}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set navigation = [
|
||||
{"href": url_for("main.show_accounts_or_dashboard"), "text": "Current service", "active": header_navigation.is_selected('accounts-or-dashboard')},
|
||||
{"href": url_for('main.get_started'), "text": "Using Notify", "active": header_navigation.is_selected('using_notify')},
|
||||
{"href": url_for('main.features'), "text": "Features", "active": header_navigation.is_selected('features')},
|
||||
{"href": url_for('main.support'), "text": "Contact us", "active": header_navigation.is_selected('support')}
|
||||
] %}
|
||||
|
||||
{% if current_user.platform_admin %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.platform_admin_splash_page'), "text": "Platform admin", "active": header_navigation.is_selected('platform-admin')}] %}
|
||||
{% else %}
|
||||
{% set navigation = navigation + [{"href": url_for('main.user_profile'), "text": "User profile", "active": header_navigation.is_selected('user-profile')}] %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_service %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active": secondary_navigation.is_selected('settings')},
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
] %}
|
||||
{% else %}
|
||||
{% set secondaryNavigation = [{"href": url_for('main.sign_out'), "text": "Sign out"}] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# usa header #}
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-logo display-flex flex-align-center flex-justify" id="-logo">
|
||||
<div class="logo-img display-flex">
|
||||
<a href="/">
|
||||
<span class="usa-sr-only">Notify.gov logo</span>
|
||||
<img src="{{ (asset_path | default('/static')) + 'images/notify-logo.png' }}" alt="Notify.gov logo" class="usa-flag-logo margin-right-1">
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{% if navigation %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation" class="usa-nav">
|
||||
<div class="usa-nav__inner">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="/static/images/usa-icons/close.svg" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion margin-right-1">
|
||||
{% for item in navigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__primary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="usa-nav__secondary margin-bottom-6">
|
||||
<ul class="usa-nav__secondary-links">
|
||||
{% if secondaryNavigation %}
|
||||
{% for item in secondaryNavigation %}
|
||||
{% if item.href and item.text %}
|
||||
<li class="usa-nav__secondary-item{{ ' is-current' if item.active }}">
|
||||
<a class="usa-nav__link {{ ' usa-current' if item.active }}" href="{{ item.href }}" {% for attribute, value in
|
||||
item.attributes %} {{attribute}}="{{value}}" {% endfor %}>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,18 +1,26 @@
|
||||
{% if help %}
|
||||
{% include 'partials/tour.html' %}
|
||||
{% else %}
|
||||
<nav id="nav-main-nav" aria-label="Main navigation" class="nav margin-bottom-4">
|
||||
<nav class="nav margin-bottom-4">
|
||||
<a class="usa-button margin-top-1 margin-bottom-5 width-full"
|
||||
href="{{ url_for('.choose_template', service_id=current_service.id) }}">Send messages</a>
|
||||
<ul class="usa-sidenav">
|
||||
{% if current_user.has_permissions() %}
|
||||
{% if current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('dashboard') }}" href="{{ url_for('.service_dashboard', service_id=current_service.id) }}">Dashboard</a></li>
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('activity') }}" href="{{ url_for('.all_jobs_activity', service_id=current_service.id) }}">Activity</a></li>
|
||||
{% endif %}
|
||||
{% if not current_user.has_permissions('view_activity') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ casework_navigation.is_selected('sent-messages') }}" href="{{ url_for('.view_notifications', service_id=current_service.id, status='sending,delivered,failed') }}">Sent messages</a></li>
|
||||
{% endif %}
|
||||
{% if current_user.has_permissions('manage_service', allow_org_user=True) %}
|
||||
{# <li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li> #}
|
||||
{% endif %}
|
||||
<!-- {% if current_user.has_permissions('manage_api_keys', 'manage_service') %}
|
||||
<li class="usa-sidenav__item"><a class="{{ main_navigation.is_selected('settings') }}" href="{{ url_for('.service_settings', service_id=current_service.id) }}">Settings</a></li>
|
||||
{% endif %} -->
|
||||
{% if current_user.has_permissions('manage_api_keys') %}
|
||||
<!-- <li><a class="usa-link{{ main_navigation.is_selected('api-integration') }}" href="{{ url_for('.api_integration', service_id=current_service.id) }}">API integration</a></li> -->
|
||||
{% endif %}
|
||||
{% elif current_user.has_permissions(allow_org_user=True) %}
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('usage') }}" href="{{ url_for('.usage', service_id=current_service.id) }}">Usage</a></li>
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ main_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_users', service_id=current_service.id) }}">Team members</a></li>
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav id="nav-org-nav" aria-label="Organization navigation" class="nav margin-bottom-4">
|
||||
<nav class="nav margin-bottom-4">
|
||||
<ul class="usa-sidenav">
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('dashboard') }}" href="{{ url_for('.organization_dashboard', org_id=current_org.id) }}">Usage</a></li>
|
||||
<li class="usa-sidenav__item"><a class="usa-link{{ org_navigation.is_selected('team-members') }}" href="{{ url_for('.manage_org_users', org_id=current_org.id) }}">Team members</a></li>
|
||||
@@ -1,4 +1,3 @@
|
||||
<nav id="nav-service-nav" aria-label="Service navigation">
|
||||
<div class="navigation-service margin-top-5 display-flex flex-align-end flex-justify border-bottom padding-bottom-1">
|
||||
{% if current_service.organization_id %}
|
||||
{% if current_user.platform_admin or
|
||||
@@ -14,4 +13,3 @@
|
||||
</div>
|
||||
<a href="{{ url_for('main.choose_account') }}" class="usa-link">Switch service</a>
|
||||
</div>
|
||||
</nav>
|
||||