commit
410273fc5b
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
||||
terraform/* @dandds
|
@ -3,7 +3,7 @@
|
||||
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||
"lines": null
|
||||
},
|
||||
"generated_at": "2019-12-06T21:22:07Z",
|
||||
"generated_at": "2020-01-09T11:35:03Z",
|
||||
"plugins_used": [
|
||||
{
|
||||
"base64_limit": 4.5,
|
||||
@ -98,7 +98,7 @@
|
||||
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 29,
|
||||
"line_number": 30,
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
@ -111,15 +111,6 @@
|
||||
"type": "Secret Keyword"
|
||||
}
|
||||
],
|
||||
"ssl/certificate-authority/ca.key": [
|
||||
{
|
||||
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 1,
|
||||
"type": "Private Key"
|
||||
}
|
||||
],
|
||||
"ssl/client-certs/atat.mil.key": [
|
||||
{
|
||||
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
|
||||
@ -170,7 +161,7 @@
|
||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||
"is_secret": false,
|
||||
"is_verified": false,
|
||||
"line_number": 656,
|
||||
"line_number": 665,
|
||||
"type": "Hex High Entropy String"
|
||||
}
|
||||
]
|
||||
|
@ -84,8 +84,7 @@ COPY --from=builder /install/celery_worker.py ./celery_worker.py
|
||||
COPY --from=builder /install/config/ ./config/
|
||||
COPY --from=builder /install/templates/ ./templates/
|
||||
COPY --from=builder /install/translations.yaml .
|
||||
COPY --from=builder /install/script/seed_roles.py ./script/seed_roles.py
|
||||
COPY --from=builder /install/script/sync-crls ./script/sync-crls
|
||||
COPY --from=builder /install/script/ ./script/
|
||||
COPY --from=builder /install/static/ ./static/
|
||||
COPY --from=builder /install/fixtures/ ./fixtures
|
||||
COPY --from=builder /install/uwsgi.ini .
|
||||
|
2
Pipfile
2
Pipfile
@ -18,7 +18,6 @@ flask-session = "*"
|
||||
flask-wtf = "*"
|
||||
pyopenssl = "*"
|
||||
requests = "*"
|
||||
apache-libcloud = "*"
|
||||
lockfile = "*"
|
||||
werkzeug = "*"
|
||||
PyYAML = "*"
|
||||
@ -30,6 +29,7 @@ azure-graphrbac = "*"
|
||||
msrestazure = "*"
|
||||
azure-mgmt-authorization = "*"
|
||||
azure-mgmt-managementgroups = "*"
|
||||
azure-mgmt-resource = "*"
|
||||
|
||||
[dev-packages]
|
||||
bandit = "*"
|
||||
|
225
Pipfile.lock
generated
225
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5"
|
||||
"sha256": "c203c47b00f413fd40056ef6d2d8e51b37ad3ff5f7693db5eb170b7f8fd43234"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -25,10 +25,10 @@
|
||||
},
|
||||
"alembic": {
|
||||
"hashes": [
|
||||
"sha256:49277bb7242192bbb9eac58fed4fe02ec6c3a2a4b4345d2171197459266482b2"
|
||||
"sha256:3b0cb1948833e062f4048992fbc97ecfaaaac24aaa0d83a1202a99fb58af8c6d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.1"
|
||||
"version": "==1.3.2"
|
||||
},
|
||||
"amqp": {
|
||||
"hashes": [
|
||||
@ -37,14 +37,6 @@
|
||||
],
|
||||
"version": "==2.5.2"
|
||||
},
|
||||
"apache-libcloud": {
|
||||
"hashes": [
|
||||
"sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f",
|
||||
"sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"azure-common": {
|
||||
"hashes": [
|
||||
"sha256:53b1195b8f20943ccc0e71a17849258f7781bc6db1c72edc7d6c055f79bd54e3",
|
||||
@ -76,6 +68,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"azure-mgmt-resource": {
|
||||
"hashes": [
|
||||
"sha256:20b3394e4dc76fbd9459723cb8c0300fb18a8c32100076f023b5470426b9f104",
|
||||
"sha256:eaea8b5d05495d1b74220052275d46b6bed93b59245bcaa747279a52e41c3bdf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0.0"
|
||||
},
|
||||
"azure-mgmt-subscription": {
|
||||
"hashes": [
|
||||
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
|
||||
@ -117,11 +117,11 @@
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:65f4d67fc1037edacecbf39fcf956da68b984cf2a6d89bd73a8a5a80e35e3dd7",
|
||||
"sha256:8a59d80235b876881d9893751f2a87936165fc1347efee66095620b3cadf533b"
|
||||
"sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f",
|
||||
"sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.4.0rc4"
|
||||
"version": "==4.4.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
@ -256,10 +256,11 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
|
||||
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"isodate": {
|
||||
"hashes": [
|
||||
@ -284,10 +285,10 @@
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:1760b54b1d15a547c9a26d3598a1c8cdaf2436386ac1f5561934bc8a3cbbbd86",
|
||||
"sha256:e7465aa85a1db889116819f08c5de29520d2fa103324dcdca5e90af345f01771"
|
||||
"sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac",
|
||||
"sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"
|
||||
],
|
||||
"version": "==4.6.6"
|
||||
"version": "==4.6.7"
|
||||
},
|
||||
"lockfile": {
|
||||
"hashes": [
|
||||
@ -338,10 +339,10 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
|
||||
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
],
|
||||
"version": "==8.0.0"
|
||||
"version": "==8.0.2"
|
||||
},
|
||||
"msrest": {
|
||||
"hashes": [
|
||||
@ -514,10 +515,10 @@
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"
|
||||
"sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.11"
|
||||
"version": "==1.3.12"
|
||||
},
|
||||
"unipath": {
|
||||
"hashes": [
|
||||
@ -677,44 +678,46 @@
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
||||
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
|
||||
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
|
||||
],
|
||||
"version": "==0.4.1"
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:2358e685d0253125da42a48396038d4c7b4cd1448c00bbc4bda0cb8c43c2a870",
|
||||
"sha256:25017cf384eeed2e6caf72efd3ec4124e32a8b7a4387600499104387975400c7",
|
||||
"sha256:2e2de9423ff8b14303a97eafddd16c479fbcc9a0b8b0be3b7c3843a3e0cf6d69",
|
||||
"sha256:324ed908e4e40a6e2451056fe502470ad4e79495cb7a03ecab94e6309c3e117e",
|
||||
"sha256:34f865a0cf6255b694a46e4383a7131c61ea72c5b4c4f81d20e522fb1e440b4b",
|
||||
"sha256:3a2bcc464b60a18f1f7167b95b2773ede93bf3722bfa59e0802717f652b6cc25",
|
||||
"sha256:48d70865266d649b6602e2ba94820d7972ef470d3b72a8fd41a3d17321feed3a",
|
||||
"sha256:50cf23523ab3a724c6905d3b60f87fa8250d9bae3995e09f49f63effa2b54f15",
|
||||
"sha256:54c84a68abd8c4c5b71878b35eb85321df41f3d144c78181867d5b026ec74994",
|
||||
"sha256:5b59d661ee7f3200aedd7b71882b7927ea7ed522df75e3853f316a79ad872a2e",
|
||||
"sha256:5ffb39624bc573177888a21fb301ccee46838c600b27d58c3e9dae495f44d34a",
|
||||
"sha256:699b3072b7f0e69ed175a88fa8b2ec7eefc4f34d490c54ed9a52feff21a15fdc",
|
||||
"sha256:79ef4a2bb862110bd585174e551a783bee5c3aa461734a2ac7429193be357589",
|
||||
"sha256:8210a6f93c4a8c6d460b402e20e38399529b99200c3318542faf6a520c9b6a5c",
|
||||
"sha256:8d30c10cfd0a6fdf0a2d5023de00ef7b329cd6ead2310c9e53eab79c209acb70",
|
||||
"sha256:97ac79ff28f2cda6ac00a803ee582b965951755f61ab43377482bfba450b619a",
|
||||
"sha256:9fe4aacacff9028ed167db108bf013510654f148d83c4857fed61d2ce0588bf2",
|
||||
"sha256:a5b6395d5957d638f8b1870561607e3c39b1a236ea6cff9eafe5b9bb1db913f2",
|
||||
"sha256:ab32c5fad6905986a7e34e3acf01180a69bb60c2aa7331815b46e51c776a1943",
|
||||
"sha256:ad67f0cfdfecbd49b9da46a7e488e6dc32a69388740b85c36a4ef4b33082cbad",
|
||||
"sha256:aedad67c30326a1af324f45833a40b97180664912deb29942459ddbe9fa0ce19",
|
||||
"sha256:b077cd0e70f41366ac1f9d09275258fa1906758a5d4f31cacc18b10dfcf90784",
|
||||
"sha256:b8ea210810d3c14aec7561f8fe0d3eec582d1088100aaa0bb8153d53d867d20f",
|
||||
"sha256:bf572722326ce6704e863447a070039a827072b7179352570859be899b9e6551",
|
||||
"sha256:c0df57e189dacd2606cae6386acf127d01d85b2bf49acd9a65543b5d6c359ddc",
|
||||
"sha256:d523e75f2a8a0b4a6a8be1287c0e0e3a561b8832b05ddd987d4cd7c62f3ad3bc",
|
||||
"sha256:e10593c60c5f0bfd8b241bf9f27ef2191a3005b73dde8ada0424f642543a1e59",
|
||||
"sha256:e9128444c83bc260aea988bf1ca6278a33ba730955bf94720468c656b61353eb",
|
||||
"sha256:f7162f2e3711f3a08a8a741f92e1f63afd58d0713177979f2cf9723dd50161cf"
|
||||
"sha256:0cd13a6e98c37b510a2d34c8281d5e1a226aaf9b65b7d770ef03c63169965351",
|
||||
"sha256:1a4b6b6a2a3a6612e6361130c2cc3dc4378d8c221752b96167ccbad94b47f3cd",
|
||||
"sha256:2ee55e6dba516ddf6f484aa83ccabbb0adf45a18892204c23486938d12258cde",
|
||||
"sha256:3be5338a2eb4ef03c57f20917e1d12a1fd10e3853fed060b6d6b677cb3745898",
|
||||
"sha256:44b783b02db03c4777d8cf71bae19eadc171a6f2a96777d916b2c30a1eb3d070",
|
||||
"sha256:475bf7c4252af0a56e1abba9606f1e54127cdf122063095c75ab04f6f99cf45e",
|
||||
"sha256:47c81ee687eafc2f1db7f03fbe99aab81330565ebc62fb3b61edfc2216a550c8",
|
||||
"sha256:4a7f8e72b18f2aca288ff02255ce32cc830bc04d993efbc87abf6beddc9e56c0",
|
||||
"sha256:50197163a22fd17f79086e087a787883b3ec9280a509807daf158dfc2a7ded02",
|
||||
"sha256:56b13000acf891f700f5067512b804d1ec8c301d627486c678b903859d07f798",
|
||||
"sha256:79388ae29c896299b3567965dbcd93255f175c17c6c7bca38614d12718c47466",
|
||||
"sha256:79fd5d3d62238c4f583b75d48d53cdae759fe04d4fb18fe8b371d88ad2b6f8be",
|
||||
"sha256:7fe3e2fde2bf1d7ce25ebcd2d3de3650b8d60d9a73ce6dcef36e20191291613d",
|
||||
"sha256:81042a24f67b96e4287774014fa27220d8a4d91af1043389e4d73892efc89ac6",
|
||||
"sha256:81326f1095c53111f8afc95da281e1414185f4a538609a77ca50bdfa39a6c207",
|
||||
"sha256:8873dc0d8f42142ea9f20c27bbdc485190fff93823c6795be661703369e5877d",
|
||||
"sha256:88d2cbcb0a112f47eef71eb95460b6995da18e6f8ca50c264585abc2c473154b",
|
||||
"sha256:91f2491aeab9599956c45a77c5666d323efdec790bfe23fcceafcd91105d585a",
|
||||
"sha256:979daa8655ae5a51e8e7a24e7d34e250ae8309fd9719490df92cbb2fe2b0422b",
|
||||
"sha256:9c871b006c878a890c6e44a5b2f3c6291335324b298c904dc0402ee92ee1f0be",
|
||||
"sha256:a6d092545e5af53e960465f652e00efbf5357adad177b2630d63978d85e46a72",
|
||||
"sha256:b5ed7837b923d1d71c4f587ae1539ccd96bfd6be9788f507dbe94dab5febbb5d",
|
||||
"sha256:ba259f68250f16d2444cbbfaddaa0bb20e1560a4fdaad50bece25c199e6af864",
|
||||
"sha256:be1d89614c6b6c36d7578496dc8625123bda2ff44f224cf8b1c45b810ee7383f",
|
||||
"sha256:c1b030a79749aa8d1f1486885040114ee56933b15ccfc90049ba266e4aa2139f",
|
||||
"sha256:c95bb147fab76f2ecde332d972d8f4138b8f2daee6c466af4ff3b4f29bd4c19e",
|
||||
"sha256:d52c1c2d7e856cecc05aa0526453cb14574f821b7f413cc279b9514750d795c1",
|
||||
"sha256:d609a6d564ad3d327e9509846c2c47f170456344521462b469e5cb39e48ba31c",
|
||||
"sha256:e1bad043c12fb58e8c7d92b3d7f2f49977dcb80a08a6d1e7a5114a11bf819fca",
|
||||
"sha256:e5a675f6829c53c87d79117a8eb656cc4a5f8918185a32fc93ba09778e90f6db",
|
||||
"sha256:fec32646b98baf4a22fdceb08703965bd16dea09051fbeb31a04b5b6e72b846c"
|
||||
],
|
||||
"version": "==5.0b1"
|
||||
"version": "==5.0"
|
||||
},
|
||||
"decorator": {
|
||||
"hashes": [
|
||||
@ -747,10 +750,10 @@
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:48c03580720e0b46538d528b1296e4e5b24a809dcaf33a7dddec719489a9edb8",
|
||||
"sha256:6327c665c0d8721280b3036d9c9e851c60092bc1f30c8394cc433f8723e2bda5"
|
||||
"sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11",
|
||||
"sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432"
|
||||
],
|
||||
"version": "==2.0.4"
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
@ -791,25 +794,26 @@
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
|
||||
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
|
||||
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"ipdb": {
|
||||
"hashes": [
|
||||
"sha256:473fdd798a099765f093231a8b1fabfa95b0b682fce12de0c74b61a4b4d8ee57"
|
||||
"sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.12.2"
|
||||
"version": "==0.12.3"
|
||||
},
|
||||
"ipython": {
|
||||
"hashes": [
|
||||
"sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51",
|
||||
"sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e"
|
||||
"sha256:190a279bd3d4fc585a611e9358a88f1048cc57fd688254a86f9461889ee152a6",
|
||||
"sha256:762d79a62b6aa96b04971e920543f558dfbeedc0468b899303c080c8068d4ac2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.10.1"
|
||||
"version": "==7.10.2"
|
||||
},
|
||||
"ipython-genutils": {
|
||||
"hashes": [
|
||||
@ -914,30 +918,29 @@
|
||||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
|
||||
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
|
||||
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||
],
|
||||
"version": "==8.0.0"
|
||||
"version": "==8.0.2"
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768",
|
||||
"sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646",
|
||||
"sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c",
|
||||
"sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939",
|
||||
"sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279",
|
||||
"sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2",
|
||||
"sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2",
|
||||
"sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640",
|
||||
"sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7",
|
||||
"sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c",
|
||||
"sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17",
|
||||
"sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78",
|
||||
"sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931",
|
||||
"sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a"
|
||||
"sha256:0308c35fd16c96a81b8dfc4d09ec63b8fa607cfec087acf5aafb44c2c45197de",
|
||||
"sha256:39f7be2f89668d21b2bbab45ce5aa15e69bf8d6f3b46f9e1cc1a88e4fcc84f3d",
|
||||
"sha256:4223f576813c79a10d0fd14192c86f1b85e3bd235c93792f22ed811a20b5ee4e",
|
||||
"sha256:4c8f812a2fbefa96185933fbe05aa035e9cf791cf3a23bbdb6a219c80b60e0b1",
|
||||
"sha256:4ea9ee847ea5bb38ea275441f3aea7eeba1b96187a3f968ee359d33d9dcc0eda",
|
||||
"sha256:573c68df69f0e399fa57866a0b72989acf0a56c4008eee59c789c2ca5ea9df03",
|
||||
"sha256:588c0e38466306aa7dbe6522ceacf37dde8b13cfa5edde90be2ce382f078875f",
|
||||
"sha256:6d1bd2e675823a19e6bf72149540ab9851bfe698b796aea698fb926ab2bedd02",
|
||||
"sha256:aa8e3bd1540dd5c39ef580ec2146a9c99c45f7c62af890095fec9e87b5ca19fb",
|
||||
"sha256:b978ba1ea90d0abe2fc720ec9a41824b7d3a1304569bd58c9038d8d61dc4dfdb",
|
||||
"sha256:c85c5367c2e8247e06cc0aba84e3633e90f48e8a0677bc51b351e138b5ff80b1",
|
||||
"sha256:ce69577b424058bfa177df27213869f37c1e964c3e1ebd3b3d54f1d10b234c4d",
|
||||
"sha256:ec6eaf98a57624d96d9916352a5bad2d73959f6358fabf43838f7d1a4d2f8389"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.750"
|
||||
"version": "==0.760"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@ -948,10 +951,10 @@
|
||||
},
|
||||
"parso": {
|
||||
"hashes": [
|
||||
"sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc",
|
||||
"sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"
|
||||
"sha256:55cf25df1a35fd88b878715874d2c4dc1ad3f0eebd1e0266a67e1f55efccfbe1",
|
||||
"sha256:5c1f7791de6bd5dbbeac8db0ef5594b36799de198b3f7f7014643b0c5536b9d3"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
"version": "==0.5.2"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@ -1063,11 +1066,11 @@
|
||||
},
|
||||
"pytest-mock": {
|
||||
"hashes": [
|
||||
"sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba",
|
||||
"sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859"
|
||||
"sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d",
|
||||
"sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.12.1"
|
||||
"version": "==1.13.0"
|
||||
},
|
||||
"pytest-watch": {
|
||||
"hashes": [
|
||||
@ -1102,21 +1105,29 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7",
|
||||
"sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7",
|
||||
"sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96",
|
||||
"sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1",
|
||||
"sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69",
|
||||
"sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910",
|
||||
"sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143",
|
||||
"sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59",
|
||||
"sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2",
|
||||
"sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66",
|
||||
"sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6",
|
||||
"sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a",
|
||||
"sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74"
|
||||
"sha256:0472acc4b6319801c1bc681d838c88ba1446f9ae199e01f6e41091c701fb3d42",
|
||||
"sha256:16709434c4e2332ee8ba26ae339aceb8ab0b24b8398ebd0f52ebc943f45c4fc2",
|
||||
"sha256:223fb63ec8dcab20b3318e93dcec4aee89e98b062934090bf29ffc374d2000a2",
|
||||
"sha256:23c3ebf05d1cd3adb26723fd598e75724e0cdb7d6a35185ac0caf061cc6edb49",
|
||||
"sha256:2404a50fb48badaf214b700f08822b68d93d79200e0aefd9569d0332d21fbfcb",
|
||||
"sha256:2af3a7a16fed6eff85c25da106effa36f61cbbe801d00ade349b53ce7619eb15",
|
||||
"sha256:37e018d3746baf159aedfc9773c3cafacbd10d354ba15484f5cfc8ed9da5748b",
|
||||
"sha256:3c9c2988d02a9238a1975c70e87c6ce94e6f36dd8e372b66f468990cfe077434",
|
||||
"sha256:47298bc8b89d1c747f0f5974aa528fc0b6b17396f1694136a224d51461279d83",
|
||||
"sha256:4eeb0fe936797ae00a085f99802642bfc722b3b4ea557e9e7849cb621ea10c91",
|
||||
"sha256:6881be0218b47ed76db033f252bab3f912dfe7ed1fe7baa9daebf51de08546a0",
|
||||
"sha256:7ac08cee5055f548eed3889e9aaef15fd00172d037949496f1f0b34acb8a7c3e",
|
||||
"sha256:7c5e2efcf079c35ff266c3f3a6708834f88f9fd04a3c16b855e036b2b7b1b543",
|
||||
"sha256:8355eaa64724a0fdb010a1654b77cb3e375dc08b7f592cc4a1c05ac606aa481c",
|
||||
"sha256:999a885f7f5194464238ad5d74b05982acee54002f3aa775d8e0e8c5fb74c06c",
|
||||
"sha256:9fd2f4813eaa3e421e82819d38e5b634d900faff7ae5a80cd89ccff407175e69",
|
||||
"sha256:a2e1e53df7dd27943da2b512895125b33fb20f81862c9fed7b3bab2a1de684d1",
|
||||
"sha256:ab43bc0836820b7900dfffc025b996784aec26ec87dc1df4f95a40398760223f",
|
||||
"sha256:ba449b56fa419fb19bf2a2438adbd2433f27087a6fe115917eaf9cfca684d5b6",
|
||||
"sha256:d3f632cefad2cf247bd845794002585e3772288bfcb0dbac59fdecd32cd38b67",
|
||||
"sha256:d51311496061863caae2cfe120cf1ef37900019b86c89c2d75f0918e0b4b8bf3"
|
||||
],
|
||||
"version": "==2019.11.1"
|
||||
"version": "==2019.12.19"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
|
@ -254,6 +254,7 @@ To generate coverage reports for the Javascript tests:
|
||||
- `SECRET_KEY`: String key which will be used to sign the session cookie. Should be a long string of random bytes. https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY
|
||||
- `SERVER_NAME`: Hostname for ATAT. Only needs to be specified in contexts where the hostname cannot be inferred from the request, such as Celery workers. https://flask.palletsprojects.com/en/1.1.x/config/#SERVER_NAME
|
||||
- `SESSION_COOKIE_NAME`: String value specifying the name to use for the session cookie. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_NAME
|
||||
- `SESSION_COOKIE_DOMAIN`: String value specifying the name to use for the session cookie. This should be set to the root domain so that it is valid for both the main site and the authentication subdomain. https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_DOMAIN
|
||||
- `SESSION_TYPE`: String value specifying the cookie storage backend. https://pythonhosted.org/Flask-Session/
|
||||
- `SESSION_USE_SIGNER`: Boolean value specifying if the cookie sid should be signed.
|
||||
- `SQLALCHEMY_ECHO`: Boolean value specifying if SQLAlchemy should log queries to stdout.
|
||||
|
@ -0,0 +1,36 @@
|
||||
"""update portfolios defense component column type
|
||||
|
||||
Revision ID: 02ac8bdcf16f
|
||||
Revises: 08f2a640e9c2
|
||||
Create Date: 2019-12-26 16:10:54.366461
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '02ac8bdcf16f' # pragma: allowlist secret
|
||||
down_revision = '08f2a640e9c2' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('portfolios', 'defense_component',
|
||||
type_=postgresql.ARRAY(sa.VARCHAR()),
|
||||
existing_type=sa.VARCHAR(),
|
||||
postgresql_using="string_to_array(defense_component, ',')::character varying[]",
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('portfolios', 'defense_component',
|
||||
type_=sa.VARCHAR(),
|
||||
existing_type=postgresql.ARRAY(sa.VARCHAR()),
|
||||
postgresql_using="defense_component[1]::character varying",
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,26 @@
|
||||
"""add uniqueness contraint to environment within an application
|
||||
|
||||
Revision ID: 08f2a640e9c2
|
||||
Revises: c487d91f1a26
|
||||
Create Date: 2019-12-16 10:43:12.331095
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '08f2a640e9c2' # pragma: allowlist secret
|
||||
down_revision = 'c487d91f1a26' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_unique_constraint('environments_name_application_id_key', 'environments', ['name', 'application_id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('environments_name_application_id_key', 'environments', type_='unique')
|
||||
# ### end Alembic commands ###
|
28
alembic/versions/5d7198d34b91_remove_users_provisional.py
Normal file
28
alembic/versions/5d7198d34b91_remove_users_provisional.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""remove users.provisional
|
||||
|
||||
Revision ID: 5d7198d34b91
|
||||
Revises: 02ac8bdcf16f
|
||||
Create Date: 2020-01-09 08:42:34.512191
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5d7198d34b91' # pragma: allowlist secret
|
||||
down_revision = '02ac8bdcf16f' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'provisional')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('provisional', sa.BOOLEAN(), autoincrement=False, nullable=True))
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,27 @@
|
||||
"""add application name and portfolio_id unique constraint
|
||||
|
||||
Revision ID: c487d91f1a26
|
||||
Revises: 3bd8552f1c57
|
||||
Create Date: 2019-12-13 14:33:23.952450
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c487d91f1a26' # pragma: allowlist secret
|
||||
down_revision = '3bd8552f1c57' # pragma: allowlist secret
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_unique_constraint('applications_name_portfolio_id_key', 'applications', ['name', 'portfolio_id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('applications_name_portfolio_id_key', 'applications', type_='unique')
|
||||
# ### end Alembic commands ###
|
@ -11,7 +11,7 @@ from atst.models import (
|
||||
ApplicationRoleStatus,
|
||||
EnvironmentRole,
|
||||
)
|
||||
from atst.utils import first_or_none
|
||||
from atst.utils import first_or_none, commit_or_raise_already_exists_error
|
||||
|
||||
|
||||
class Applications(BaseDomainClass):
|
||||
@ -28,7 +28,7 @@ class Applications(BaseDomainClass):
|
||||
if environment_names:
|
||||
Environments.create_many(user, application, environment_names)
|
||||
|
||||
db.session.commit()
|
||||
commit_or_raise_already_exists_error(message="application")
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
@ -53,9 +53,9 @@ class Applications(BaseDomainClass):
|
||||
Environments.create_many(
|
||||
g.current_user, application, new_data["environment_names"]
|
||||
)
|
||||
db.session.add(application)
|
||||
db.session.commit()
|
||||
|
||||
db.session.add(application)
|
||||
commit_or_raise_already_exists_error(message="application")
|
||||
return application
|
||||
|
||||
@classmethod
|
||||
|
@ -1,4 +1,4 @@
|
||||
from flask import g, redirect, url_for, session, request
|
||||
from flask import g, redirect, url_for, session, request, current_app as app
|
||||
|
||||
from atst.domain.users import Users
|
||||
|
||||
@ -59,8 +59,10 @@ def get_last_login():
|
||||
|
||||
def logout():
|
||||
if session.get("user_id"): # pragma: no branch
|
||||
dod_id = g.current_user.dod_id
|
||||
del session["user_id"]
|
||||
del session["last_login"]
|
||||
app.logger.info(f"user with EDIPI {dod_id} has logged out")
|
||||
|
||||
|
||||
def _unprotected_route(request):
|
||||
|
@ -276,7 +276,7 @@ def existing_crl_modification_time(crl):
|
||||
prev_time = os.path.getmtime(crl)
|
||||
buffered = prev_time + MODIFIED_TIME_BUFFER
|
||||
mod_time = prev_time if pendulum.now().timestamp() < buffered else buffered
|
||||
dt = pendulum.from_timestamp(mod_time, tz="GMT")
|
||||
dt = pendulum.from_timestamp(mod_time, tz="UTC")
|
||||
return dt.format("ddd, DD MMM YYYY HH:mm:ss zz")
|
||||
|
||||
else:
|
||||
|
@ -6,6 +6,7 @@ from atst.models.user import User
|
||||
from atst.models.application import Application
|
||||
from atst.models.environment import Environment
|
||||
from atst.models.environment_role import EnvironmentRole
|
||||
from .policy import AzurePolicyManager
|
||||
|
||||
|
||||
class GeneralCSPException(Exception):
|
||||
@ -401,6 +402,7 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
|
||||
class AzureSDKProvider(object):
|
||||
def __init__(self):
|
||||
from azure.mgmt import subscription, authorization, managementgroups
|
||||
from azure.mgmt.resource import policy
|
||||
import azure.graphrbac as graphrbac
|
||||
import azure.common.credentials as credentials
|
||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||
@ -410,6 +412,7 @@ class AzureSDKProvider(object):
|
||||
self.managementgroups = managementgroups
|
||||
self.graphrbac = graphrbac
|
||||
self.credentials = credentials
|
||||
self.policy = policy
|
||||
# may change to a JEDI cloud
|
||||
self.cloud = AZURE_PUBLIC_CLOUD
|
||||
|
||||
@ -427,6 +430,8 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
else:
|
||||
self.sdk = azure_sdk_provider
|
||||
|
||||
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
|
||||
|
||||
def create_environment(
|
||||
self, auth_credentials: Dict, user: User, environment: Environment
|
||||
):
|
||||
@ -561,6 +566,51 @@ class AzureCloudProvider(CloudProviderInterface):
|
||||
# but we just don't have a valid ID
|
||||
pass
|
||||
|
||||
AZURE_MANAGEMENT_API = "https://management.azure.com"
|
||||
|
||||
def _create_policy_definition(
|
||||
self, credentials, subscription_id, management_group_id, properties,
|
||||
):
|
||||
"""
|
||||
Requires credentials that have AZURE_MANAGEMENT_API
|
||||
specified as the resource. The Service Principal
|
||||
specified in the credentials must have the "Resource
|
||||
Policy Contributor" role assigned with a scope at least
|
||||
as high as the management group specified by
|
||||
management_group_id.
|
||||
|
||||
Arguments:
|
||||
credentials -- ServicePrincipalCredentials
|
||||
subscription_id -- str, ID of the subscription (just the UUID, not the path)
|
||||
management_group_id -- str, ID of the management group (just the UUID, not the path)
|
||||
properties -- dictionary, the "properties" section of a valid Azure policy definition document
|
||||
|
||||
Returns:
|
||||
azure.mgmt.resource.policy.[api version].models.PolicyDefinition: the PolicyDefinition object provided to Azure
|
||||
|
||||
Raises:
|
||||
TBD
|
||||
"""
|
||||
# TODO: which subscription would this be?
|
||||
client = self.sdk.policy.PolicyClient(credentials, subscription_id)
|
||||
|
||||
definition = client.policy_definitions.models.PolicyDefinition(
|
||||
policy_type=properties.get("policyType"),
|
||||
mode=properties.get("mode"),
|
||||
display_name=properties.get("displayName"),
|
||||
description=properties.get("description"),
|
||||
policy_rule=properties.get("policyRule"),
|
||||
parameters=properties.get("parameters"),
|
||||
)
|
||||
|
||||
name = properties.get("displayName")
|
||||
|
||||
return client.policy_definitions.create_or_update_at_management_group(
|
||||
policy_definition_name=name,
|
||||
parameters=definition,
|
||||
management_group_id=management_group_id,
|
||||
)
|
||||
|
||||
def _get_management_service_principal(self):
|
||||
# we really should be using graph.microsoft.com, but i'm getting
|
||||
# "expired token" errors for that
|
||||
|
47
atst/domain/csp/policy.py
Normal file
47
atst/domain/csp/policy.py
Normal file
@ -0,0 +1,47 @@
|
||||
from glob import glob
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from os.path import join as path_join
|
||||
|
||||
|
||||
class AzurePolicyManager:
|
||||
def __init__(self, static_policy_location):
|
||||
self._static_policy_location = static_policy_location
|
||||
|
||||
@property
|
||||
def portfolio_definitions(self):
|
||||
if getattr(self, "_portfolio_definitions", None) is None:
|
||||
portfolio_files = self._glob_json("portfolios")
|
||||
self._portfolio_definitions = self._load_policies(portfolio_files)
|
||||
|
||||
return self._portfolio_definitions
|
||||
|
||||
@property
|
||||
def application_definitions(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def environment_definitions(self):
|
||||
pass
|
||||
|
||||
def _glob_json(self, path):
|
||||
return glob(path_join(self._static_policy_location, "portfolios", "*.json"))
|
||||
|
||||
def _load_policies(self, json_policies):
|
||||
return [self._load_policy(pol) for pol in json_policies]
|
||||
|
||||
def _load_policy(self, policy_file):
|
||||
with open(policy_file, "r") as file_:
|
||||
doc = json.loads(file_.read())
|
||||
return AzurePolicy(
|
||||
definition_point=doc["definitionPoint"],
|
||||
definition=doc["policyDefinition"],
|
||||
parameters=doc["parameters"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AzurePolicy:
|
||||
definition_point: str
|
||||
definition: dict
|
||||
parameters: dict
|
@ -34,16 +34,24 @@ class MockReportingProvider:
|
||||
}
|
||||
]
|
||||
"""
|
||||
if portfolio.name in cls.FIXTURE_SPEND_DATA:
|
||||
applications = cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]
|
||||
|
||||
fixture_apps = cls.FIXTURE_SPEND_DATA.get(portfolio.name, {}).get(
|
||||
"applications", []
|
||||
)
|
||||
|
||||
for application in portfolio.applications:
|
||||
if application.name not in [app["name"] for app in fixture_apps]:
|
||||
fixture_apps.append({"name": application.name, "environments": []})
|
||||
|
||||
return sorted(
|
||||
[
|
||||
cls._get_application_monthly_totals(application)
|
||||
for application in applications
|
||||
cls._get_application_monthly_totals(portfolio, fixture_app)
|
||||
for fixture_app in fixture_apps
|
||||
if fixture_app["name"]
|
||||
in [application.name for application in portfolio.applications]
|
||||
],
|
||||
key=lambda app: app["name"],
|
||||
)
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _get_environment_monthly_totals(cls, environment):
|
||||
@ -64,7 +72,7 @@ class MockReportingProvider:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_application_monthly_totals(cls, application):
|
||||
def _get_application_monthly_totals(cls, portfolio, fixture_app):
|
||||
"""
|
||||
returns a dictionary that represents spending totals for an application
|
||||
and its environments e.g.
|
||||
@ -83,19 +91,28 @@ class MockReportingProvider:
|
||||
]
|
||||
}
|
||||
"""
|
||||
environments = sorted(
|
||||
[
|
||||
application_envs = [
|
||||
env
|
||||
for env in portfolio.all_environments
|
||||
if env.application.name == fixture_app["name"]
|
||||
]
|
||||
|
||||
environments = [
|
||||
cls._get_environment_monthly_totals(env)
|
||||
for env in application["environments"]
|
||||
],
|
||||
key=lambda env: env["name"],
|
||||
)
|
||||
for env in fixture_app["environments"]
|
||||
if env["name"] in [e.name for e in application_envs]
|
||||
]
|
||||
|
||||
for env in application_envs:
|
||||
if env.name not in [env["name"] for env in environments]:
|
||||
environments.append({"name": env.name})
|
||||
|
||||
return {
|
||||
"name": application["name"],
|
||||
"this_month": sum(env["this_month"] for env in environments),
|
||||
"last_month": sum(env["last_month"] for env in environments),
|
||||
"total": sum(env["total"] for env in environments),
|
||||
"environments": environments,
|
||||
"name": fixture_app["name"],
|
||||
"this_month": sum(env.get("this_month", 0) for env in environments),
|
||||
"last_month": sum(env.get("last_month", 0) for env in environments),
|
||||
"total": sum(env.get("total", 0) for env in environments),
|
||||
"environments": sorted(environments, key=lambda env: env["name"]),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -12,6 +12,7 @@ from atst.models import (
|
||||
CLIN,
|
||||
)
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.utils import commit_or_raise_already_exists_error
|
||||
|
||||
from .exceptions import NotFoundError, DisabledError
|
||||
|
||||
@ -21,7 +22,7 @@ class Environments(object):
|
||||
def create(cls, user, application, name):
|
||||
environment = Environment(application=application, name=name, creator=user)
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
commit_or_raise_already_exists_error(message="environment")
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
@ -39,7 +40,8 @@ class Environments(object):
|
||||
if name is not None:
|
||||
environment.name = name
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
commit_or_raise_already_exists_error(message="environment")
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
def get(cls, environment_id):
|
||||
|
@ -75,10 +75,10 @@ class Portfolios(object):
|
||||
permission_sets = PortfolioRoles._permission_sets_for_names(
|
||||
member_data.get("permission_sets", [])
|
||||
)
|
||||
role = PortfolioRole(portfolio_id=portfolio.id, permission_sets=permission_sets)
|
||||
role = PortfolioRole(portfolio=portfolio, permission_sets=permission_sets)
|
||||
|
||||
invitation = PortfolioInvitations.create(
|
||||
inviter=inviter, role=role, member_data=member_data
|
||||
inviter=inviter, role=role, member_data=member_data["user_data"]
|
||||
)
|
||||
|
||||
PortfoliosQuery.add_and_commit(role)
|
||||
@ -107,4 +107,7 @@ class Portfolios(object):
|
||||
if "name" in new_data:
|
||||
portfolio.name = new_data["name"]
|
||||
|
||||
if "description" in new_data:
|
||||
portfolio.description = new_data["description"]
|
||||
|
||||
PortfoliosQuery.add_and_commit(portfolio)
|
||||
|
@ -1,11 +1,10 @@
|
||||
import datetime
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from atst.database import db
|
||||
from atst.models.clin import CLIN
|
||||
from atst.models.task_order import TaskOrder, SORT_ORDERING
|
||||
from . import BaseDomainClass
|
||||
from .exceptions import AlreadyExistsError
|
||||
from atst.utils import commit_or_raise_already_exists_error
|
||||
|
||||
|
||||
class TaskOrders(BaseDomainClass):
|
||||
@ -16,15 +15,8 @@ class TaskOrders(BaseDomainClass):
|
||||
def create(cls, portfolio_id, number, clins, pdf):
|
||||
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
|
||||
db.session.add(task_order)
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise AlreadyExistsError("task_order")
|
||||
|
||||
commit_or_raise_already_exists_error(message="task_order")
|
||||
TaskOrders.create_clins(task_order.id, clins)
|
||||
|
||||
return task_order
|
||||
|
||||
@classmethod
|
||||
@ -42,12 +34,7 @@ class TaskOrders(BaseDomainClass):
|
||||
task_order.number = number
|
||||
db.session.add(task_order)
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise AlreadyExistsError("task_order")
|
||||
|
||||
commit_or_raise_already_exists_error(message="task_order")
|
||||
return task_order
|
||||
|
||||
@classmethod
|
||||
|
@ -117,12 +117,3 @@ class Users(object):
|
||||
user.last_session_id = session_id
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def finalize(cls, user):
|
||||
user.provisional = False
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
return user
|
||||
|
@ -23,20 +23,10 @@ class PortfolioForm(BaseForm):
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class PortfolioCreationForm(BaseForm):
|
||||
name = StringField(
|
||||
translate("forms.portfolio.name.label"),
|
||||
validators=[
|
||||
Length(
|
||||
min=4,
|
||||
max=100,
|
||||
message=translate("forms.portfolio.name.length_validation_message"),
|
||||
)
|
||||
],
|
||||
)
|
||||
description = TextAreaField(translate("forms.portfolio.description.label"),)
|
||||
|
||||
|
||||
class PortfolioCreationForm(PortfolioForm):
|
||||
defense_component = SelectMultipleField(
|
||||
choices=SERVICE_BRANCHES,
|
||||
widget=ListWidget(prefix_label=False),
|
||||
|
@ -1,76 +1,59 @@
|
||||
from wtforms.validators import Required
|
||||
from wtforms.fields import StringField, FormField, FieldList, HiddenField
|
||||
from wtforms.fields import BooleanField, FormField
|
||||
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from .forms import BaseForm
|
||||
from .member import NewForm as BaseNewMemberForm
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.forms.fields import SelectField
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class PermissionsForm(BaseForm):
|
||||
member_name = StringField()
|
||||
member_id = HiddenField()
|
||||
perms_app_mgmt = SelectField(
|
||||
translate("forms.new_member.app_mgmt"),
|
||||
choices=[
|
||||
(
|
||||
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
translate("common.view"),
|
||||
),
|
||||
(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
translate("common.edit"),
|
||||
),
|
||||
],
|
||||
perms_app_mgmt = BooleanField(
|
||||
translate("forms.new_member.app_mgmt.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.app_mgmt.description"),
|
||||
)
|
||||
perms_funding = SelectField(
|
||||
translate("forms.new_member.funding"),
|
||||
choices=[
|
||||
(PermissionSets.VIEW_PORTFOLIO_FUNDING, translate("common.view")),
|
||||
(PermissionSets.EDIT_PORTFOLIO_FUNDING, translate("common.edit")),
|
||||
],
|
||||
perms_funding = BooleanField(
|
||||
translate("forms.new_member.funding.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.funding.description"),
|
||||
)
|
||||
perms_reporting = SelectField(
|
||||
translate("forms.new_member.reporting"),
|
||||
choices=[
|
||||
(PermissionSets.VIEW_PORTFOLIO_REPORTS, translate("common.view")),
|
||||
(PermissionSets.EDIT_PORTFOLIO_REPORTS, translate("common.edit")),
|
||||
],
|
||||
perms_reporting = BooleanField(
|
||||
translate("forms.new_member.reporting.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.reporting.description"),
|
||||
)
|
||||
perms_portfolio_mgmt = SelectField(
|
||||
translate("forms.new_member.portfolio_mgmt"),
|
||||
choices=[
|
||||
(PermissionSets.VIEW_PORTFOLIO_ADMIN, translate("common.view")),
|
||||
(PermissionSets.EDIT_PORTFOLIO_ADMIN, translate("common.edit")),
|
||||
],
|
||||
perms_portfolio_mgmt = BooleanField(
|
||||
translate("forms.new_member.portfolio_mgmt.label"),
|
||||
default=False,
|
||||
description=translate("forms.new_member.portfolio_mgmt.description"),
|
||||
)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
_data = super().data
|
||||
_data["permission_sets"] = []
|
||||
for field in _data:
|
||||
if "perms" in field:
|
||||
_data["permission_sets"].append(_data[field])
|
||||
_data.pop("csrf_token", None)
|
||||
perm_sets = []
|
||||
|
||||
if _data["perms_app_mgmt"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT)
|
||||
|
||||
if _data["perms_funding"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_FUNDING)
|
||||
|
||||
if _data["perms_reporting"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_REPORTS)
|
||||
|
||||
if _data["perms_portfolio_mgmt"]:
|
||||
perm_sets.append(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
|
||||
_data["permission_sets"] = perm_sets
|
||||
return _data
|
||||
|
||||
|
||||
class MembersPermissionsForm(BaseForm):
|
||||
members_permissions = FieldList(FormField(PermissionsForm))
|
||||
|
||||
|
||||
class NewForm(BaseForm):
|
||||
class NewForm(PermissionsForm):
|
||||
user_data = FormField(BaseNewMemberForm)
|
||||
permission_sets = FormField(PermissionsForm)
|
||||
|
||||
@property
|
||||
def update_data(self):
|
||||
return {
|
||||
"permission_sets": self.data.get("permission_sets").get("permission_sets"),
|
||||
**self.data.get("user_data"),
|
||||
}
|
||||
|
||||
|
||||
class AssignPPOCForm(PermissionsForm):
|
||||
|
@ -151,3 +151,6 @@ class SignatureForm(BaseForm):
|
||||
translate("task_orders.sign.digital_signature_description"),
|
||||
validators=[Required()],
|
||||
)
|
||||
confirm = BooleanField(
|
||||
translate("task_orders.sign.confirmation_description"), validators=[Required()],
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import and_, Column, ForeignKey, String
|
||||
from sqlalchemy import and_, Column, ForeignKey, String, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship, synonym
|
||||
|
||||
from atst.models.base import Base
|
||||
@ -34,6 +34,11 @@ class Application(
|
||||
),
|
||||
)
|
||||
members = synonym("roles")
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"name", "portfolio_id", name="applications_name_portfolio_id_key"
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, ForeignKey, String, TIMESTAMP
|
||||
from sqlalchemy import Column, ForeignKey, String, TIMESTAMP, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from enum import Enum
|
||||
@ -38,6 +38,12 @@ class Environment(
|
||||
primaryjoin="and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"name", "application_id", name="environments_name_application_id_key"
|
||||
),
|
||||
)
|
||||
|
||||
class ProvisioningStatus(Enum):
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
|
@ -1,5 +1,6 @@
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.types import ARRAY
|
||||
from itertools import chain
|
||||
|
||||
from atst.models.base import Base
|
||||
@ -20,7 +21,7 @@ class Portfolio(
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
defense_component = Column(
|
||||
String, nullable=False
|
||||
ARRAY(String), nullable=False
|
||||
) # Department of Defense Component
|
||||
|
||||
applications = relationship(
|
||||
|
@ -12,17 +12,6 @@ from atst.utils import first_or_none
|
||||
from atst.models.mixins.auditable import record_permission_sets_updates
|
||||
|
||||
|
||||
MEMBER_STATUSES = {
|
||||
"active": "Active",
|
||||
"revoked": "Invite revoked",
|
||||
"expired": "Invite expired",
|
||||
"error": "Error on invite",
|
||||
"pending": "Pending",
|
||||
"unknown": "Unknown errors",
|
||||
"disabled": "Disabled",
|
||||
}
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = "active"
|
||||
DISABLED = "disabled"
|
||||
@ -90,23 +79,23 @@ class PortfolioRole(
|
||||
@property
|
||||
def display_status(self):
|
||||
if self.status == Status.ACTIVE:
|
||||
return MEMBER_STATUSES["active"]
|
||||
return "active"
|
||||
elif self.status == Status.DISABLED:
|
||||
return MEMBER_STATUSES["disabled"]
|
||||
return "disabled"
|
||||
elif self.latest_invitation:
|
||||
if self.latest_invitation.is_revoked:
|
||||
return MEMBER_STATUSES["revoked"]
|
||||
return "invite_revoked"
|
||||
elif self.latest_invitation.is_rejected_wrong_user:
|
||||
return MEMBER_STATUSES["error"]
|
||||
return "invite_error"
|
||||
elif (
|
||||
self.latest_invitation.is_rejected_expired
|
||||
or self.latest_invitation.is_expired
|
||||
):
|
||||
return MEMBER_STATUSES["expired"]
|
||||
return "invite_expired"
|
||||
else:
|
||||
return MEMBER_STATUSES["pending"]
|
||||
return "invite_pending"
|
||||
else:
|
||||
return MEMBER_STATUSES["unknown"]
|
||||
return "unknown"
|
||||
|
||||
def has_permission_set(self, perm_set_name):
|
||||
return first_or_none(
|
||||
|
@ -9,7 +9,7 @@ from atst.models.base import Base
|
||||
import atst.models.types as types
|
||||
import atst.models.mixins as mixins
|
||||
from atst.models.attachment import Attachment
|
||||
from atst.utils.clock import Clock
|
||||
from pendulum import today
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
@ -83,26 +83,10 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
def is_active(self):
|
||||
return self.status == Status.ACTIVE
|
||||
|
||||
@property
|
||||
def is_upcoming(self):
|
||||
return self.status == Status.UPCOMING
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
return self.status == Status.EXPIRED
|
||||
|
||||
@property
|
||||
def is_unsigned(self):
|
||||
return self.status == Status.UNSIGNED
|
||||
|
||||
@property
|
||||
def has_begun(self):
|
||||
return self.start_date is not None and Clock.today() >= self.start_date
|
||||
|
||||
@property
|
||||
def has_ended(self):
|
||||
return self.start_date is not None and Clock.today() >= self.end_date
|
||||
|
||||
@property
|
||||
def clins_are_completed(self):
|
||||
return all([len(self.clins), (clin.is_completed for clin in self.clins)])
|
||||
@ -117,17 +101,17 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
today = Clock.today()
|
||||
todays_date = today(tz="UTC").date()
|
||||
|
||||
if not self.is_completed and not self.is_signed:
|
||||
return Status.DRAFT
|
||||
elif self.is_completed and not self.is_signed:
|
||||
return Status.UNSIGNED
|
||||
elif today < self.start_date:
|
||||
elif todays_date < self.start_date:
|
||||
return Status.UPCOMING
|
||||
elif today >= self.end_date:
|
||||
elif todays_date > self.end_date:
|
||||
return Status.EXPIRED
|
||||
elif self.start_date <= today < self.end_date:
|
||||
elif self.start_date <= todays_date <= self.end_date:
|
||||
return Status.ACTIVE
|
||||
|
||||
@property
|
||||
@ -141,39 +125,25 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
@property
|
||||
def days_to_expiration(self):
|
||||
if self.end_date:
|
||||
return (self.end_date - Clock.today()).days
|
||||
return (self.end_date - today(tz="UTC").date()).days
|
||||
|
||||
@property
|
||||
def total_obligated_funds(self):
|
||||
total = 0
|
||||
for clin in self.clins:
|
||||
if clin.obligated_amount is not None:
|
||||
total += clin.obligated_amount
|
||||
return total
|
||||
return sum(
|
||||
(clin.obligated_amount for clin in self.clins if clin.obligated_amount)
|
||||
)
|
||||
|
||||
@property
|
||||
def total_contract_amount(self):
|
||||
total = 0
|
||||
for clin in self.clins:
|
||||
if clin.total_amount is not None:
|
||||
total += clin.total_amount
|
||||
return total
|
||||
|
||||
@property
|
||||
# TODO delete when we delete task_order_review flow
|
||||
def budget(self):
|
||||
return 100000
|
||||
|
||||
@property
|
||||
def balance(self):
|
||||
# TODO: fix task order -- reimplement using CLINs
|
||||
# Faked for display purposes
|
||||
return 50
|
||||
return sum((clin.total_amount for clin in self.clins if clin.total_amount))
|
||||
|
||||
@property
|
||||
def invoiced_funds(self):
|
||||
# TODO: implement this using reporting data from the CSP
|
||||
if self.is_active:
|
||||
return self.total_obligated_funds * Decimal(0.75)
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def display_status(self):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from sqlalchemy import String, ForeignKey, Column, Date, Boolean, Table, TIMESTAMP
|
||||
from sqlalchemy import String, ForeignKey, Column, Date, Table, TIMESTAMP
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.event import listen
|
||||
@ -67,8 +67,6 @@ class User(
|
||||
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
last_session_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
provisional = Column(Boolean)
|
||||
|
||||
cloud_id = Column(String)
|
||||
|
||||
REQUIRED_FIELDS = [
|
||||
|
@ -19,6 +19,7 @@ from werkzeug.exceptions import NotFound
|
||||
from atst.domain.users import Users
|
||||
from atst.domain.authnid import AuthenticationContext
|
||||
from atst.domain.auth import logout as _logout
|
||||
from atst.domain.exceptions import UnauthenticatedError
|
||||
from atst.utils.flash import formatted_flash as flash
|
||||
|
||||
|
||||
@ -64,11 +65,15 @@ def catch_all(path):
|
||||
raise NotFound()
|
||||
|
||||
|
||||
def _client_s_dn():
|
||||
return request.environ.get("HTTP_X_SSL_CLIENT_S_DN")
|
||||
|
||||
|
||||
def _make_authentication_context():
|
||||
return AuthenticationContext(
|
||||
crl_cache=app.crl_cache,
|
||||
auth_status=request.environ.get("HTTP_X_SSL_CLIENT_VERIFY"),
|
||||
sdn=request.environ.get("HTTP_X_SSL_CLIENT_S_DN"),
|
||||
sdn=_client_s_dn(),
|
||||
cert=request.environ.get("HTTP_X_SSL_CLIENT_CERT"),
|
||||
)
|
||||
|
||||
@ -89,19 +94,24 @@ def current_user_setup(user):
|
||||
session["user_id"] = user.id
|
||||
session["last_login"] = user.last_login
|
||||
app.session_limiter.on_login(user)
|
||||
app.logger.info(f"authentication succeeded for user with EDIPI {user.dod_id}")
|
||||
Users.update_last_login(user)
|
||||
|
||||
|
||||
@bp.route("/login-redirect")
|
||||
def login_redirect():
|
||||
try:
|
||||
auth_context = _make_authentication_context()
|
||||
auth_context.authenticate()
|
||||
|
||||
user = auth_context.get_user()
|
||||
|
||||
if user.provisional:
|
||||
Users.finalize(user)
|
||||
|
||||
current_user_setup(user)
|
||||
except UnauthenticatedError as err:
|
||||
app.logger.info(
|
||||
f"authentication failed for subject distinguished name {_client_s_dn()}"
|
||||
)
|
||||
raise err
|
||||
|
||||
return redirect(redirect_after_login_url())
|
||||
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
from flask import redirect, render_template, request as http_request, url_for, g
|
||||
from flask import redirect, render_template, request as http_request, url_for
|
||||
|
||||
from .blueprint import applications_bp
|
||||
from atst.domain.applications import Applications
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
|
||||
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
from atst.models.permissions import Permissions
|
||||
@ -12,6 +11,7 @@ from atst.routes.applications.settings import (
|
||||
get_new_member_form,
|
||||
handle_create_member,
|
||||
handle_update_member,
|
||||
handle_update_application,
|
||||
)
|
||||
|
||||
|
||||
@ -64,17 +64,9 @@ def create_or_update_new_application_step_1(portfolio_id=None, application_id=No
|
||||
form = get_new_application_form(
|
||||
{**http_request.form}, NameAndDescriptionForm, application_id
|
||||
)
|
||||
application = handle_update_application(form, application_id, portfolio_id)
|
||||
|
||||
if form.validate():
|
||||
application = None
|
||||
if application_id:
|
||||
application = Applications.get(application_id)
|
||||
application = Applications.update(application, form.data)
|
||||
flash("application_updated", application_name=application.name)
|
||||
else:
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
application = Applications.create(g.current_user, portfolio, **form.data)
|
||||
flash("application_created", application_name=application.name)
|
||||
if application:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.update_new_application_step_2",
|
||||
|
@ -1,4 +1,10 @@
|
||||
from flask import redirect, render_template, request as http_request, url_for, g
|
||||
from flask import (
|
||||
redirect,
|
||||
render_template,
|
||||
request as http_request,
|
||||
url_for,
|
||||
g,
|
||||
)
|
||||
|
||||
from .blueprint import applications_bp
|
||||
from atst.domain.exceptions import AlreadyExistsError
|
||||
@ -10,6 +16,7 @@ from atst.domain.csp.cloud import GeneralCSPException
|
||||
from atst.domain.common import Paginator
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
from atst.domain.invitations import ApplicationInvitations
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm
|
||||
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
|
||||
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
||||
@ -245,16 +252,59 @@ def handle_update_member(application_id, application_role_id, form_data):
|
||||
# TODO: flash error message
|
||||
|
||||
|
||||
def handle_update_environment(form, application=None, environment=None):
|
||||
if form.validate():
|
||||
try:
|
||||
if environment:
|
||||
environment = Environments.update(
|
||||
environment=environment, name=form.name.data
|
||||
)
|
||||
flash("application_environments_updated")
|
||||
else:
|
||||
environment = Environments.create(
|
||||
g.current_user, application=application, name=form.name.data
|
||||
)
|
||||
flash("environment_added", environment_name=form.name.data)
|
||||
|
||||
return environment
|
||||
|
||||
except AlreadyExistsError:
|
||||
flash("application_environments_name_error", name=form.name.data)
|
||||
return False
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def handle_update_application(form, application_id=None, portfolio_id=None):
|
||||
if form.validate():
|
||||
application = None
|
||||
|
||||
try:
|
||||
if application_id:
|
||||
application = Applications.get(application_id)
|
||||
application = Applications.update(application, form.data)
|
||||
flash("application_updated", application_name=application.name)
|
||||
else:
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
application = Applications.create(
|
||||
g.current_user, portfolio, **form.data
|
||||
)
|
||||
flash("application_created", application_name=application.name)
|
||||
|
||||
return application
|
||||
|
||||
except AlreadyExistsError:
|
||||
flash("application_name_error", name=form.data["name"])
|
||||
return False
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/settings")
|
||||
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
|
||||
def settings(application_id):
|
||||
application = Applications.get(application_id)
|
||||
|
||||
return render_settings_page(
|
||||
application=application,
|
||||
active_toggler=http_request.args.get("active_toggler"),
|
||||
active_toggler_section=http_request.args.get("active_toggler_section"),
|
||||
)
|
||||
return render_settings_page(application=application,)
|
||||
|
||||
|
||||
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
|
||||
@ -264,31 +314,21 @@ def update_environment(environment_id):
|
||||
application = environment.application
|
||||
|
||||
env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form)
|
||||
updated_environment = handle_update_environment(
|
||||
form=env_form, application=application, environment=environment
|
||||
)
|
||||
|
||||
if env_form.validate():
|
||||
Environments.update(environment=environment, name=env_form.name.data)
|
||||
|
||||
flash("application_environments_updated")
|
||||
|
||||
if updated_environment:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
application_id=application.id,
|
||||
fragment="application-environments",
|
||||
_anchor="application-environments",
|
||||
active_toggler=environment.id,
|
||||
active_toggler_section="edit",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (
|
||||
render_settings_page(
|
||||
application=application,
|
||||
active_toggler=environment.id,
|
||||
active_toggler_section="edit",
|
||||
),
|
||||
400,
|
||||
)
|
||||
return (render_settings_page(application=application, show_flash=True), 400)
|
||||
|
||||
|
||||
@applications_bp.route(
|
||||
@ -298,14 +338,9 @@ def update_environment(environment_id):
|
||||
def new_environment(application_id):
|
||||
application = Applications.get(application_id)
|
||||
env_form = EditEnvironmentForm(formdata=http_request.form)
|
||||
environment = handle_update_environment(form=env_form, application=application)
|
||||
|
||||
if env_form.validate():
|
||||
Environments.create(
|
||||
g.current_user, application=application, name=env_form.name.data
|
||||
)
|
||||
|
||||
flash("environment_added", environment_name=env_form.data["name"])
|
||||
|
||||
if environment:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.settings",
|
||||
@ -315,7 +350,7 @@ def new_environment(application_id):
|
||||
)
|
||||
)
|
||||
else:
|
||||
return (render_settings_page(application=application), 400)
|
||||
return (render_settings_page(application=application, show_flash=True), 400)
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/edit", methods=["POST"])
|
||||
@ -323,10 +358,9 @@ def new_environment(application_id):
|
||||
def update(application_id):
|
||||
application = Applications.get(application_id)
|
||||
form = NameAndDescriptionForm(http_request.form)
|
||||
if form.validate():
|
||||
application_data = form.data
|
||||
Applications.update(application, application_data)
|
||||
updated_application = handle_update_application(form, application_id)
|
||||
|
||||
if updated_application:
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.portfolio_applications",
|
||||
@ -334,21 +368,9 @@ def update(application_id):
|
||||
)
|
||||
)
|
||||
else:
|
||||
return render_settings_page(application=application, application_form=form)
|
||||
|
||||
|
||||
@applications_bp.route("/applications/<application_id>/delete", methods=["POST"])
|
||||
@user_can(Permissions.DELETE_APPLICATION, message="delete application")
|
||||
def delete(application_id):
|
||||
application = Applications.get(application_id)
|
||||
Applications.delete(application)
|
||||
|
||||
flash("application_deleted", application_name=application.name)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"applications.portfolio_applications", portfolio_id=application.portfolio_id
|
||||
)
|
||||
return (
|
||||
render_settings_page(application=application, show_flash=True),
|
||||
400,
|
||||
)
|
||||
|
||||
|
||||
|
@ -17,63 +17,51 @@ from atst.utils.flash import formatted_flash as flash
|
||||
from atst.domain.exceptions import UnauthorizedError
|
||||
|
||||
|
||||
def permission_str(member, edit_perm_set, view_perm_set):
|
||||
if member.has_permission_set(edit_perm_set):
|
||||
return edit_perm_set
|
||||
else:
|
||||
return view_perm_set
|
||||
|
||||
|
||||
def serialize_member_form_data(member):
|
||||
return {
|
||||
"member_name": member.full_name,
|
||||
"member_id": member.id,
|
||||
"perms_app_mgmt": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
||||
def filter_perm_sets_data(member):
|
||||
perm_sets_data = {
|
||||
"perms_portfolio_mgmt": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||
),
|
||||
"perms_funding": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_FUNDING,
|
||||
PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
||||
"perms_app_mgmt": bool(
|
||||
member.has_permission_set(
|
||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
||||
)
|
||||
),
|
||||
"perms_reporting": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_REPORTS,
|
||||
PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
||||
"perms_funding": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING)
|
||||
),
|
||||
"perms_portfolio_mgmt": permission_str(
|
||||
member,
|
||||
PermissionSets.EDIT_PORTFOLIO_ADMIN,
|
||||
PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
||||
"perms_reporting": bool(
|
||||
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
|
||||
),
|
||||
}
|
||||
|
||||
return perm_sets_data
|
||||
|
||||
def get_members_data(portfolio):
|
||||
members = sorted(
|
||||
[serialize_member_form_data(member) for member in portfolio.members],
|
||||
key=lambda member: member["member_name"],
|
||||
|
||||
def filter_members_data(members_list, portfolio):
|
||||
members_data = []
|
||||
for member in members_list:
|
||||
members_data.append(
|
||||
{
|
||||
"role_id": member.id,
|
||||
"user_name": member.user_name,
|
||||
"permission_sets": filter_perm_sets_data(member),
|
||||
"status": member.display_status,
|
||||
"ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets,
|
||||
# add in stuff here for forms
|
||||
}
|
||||
)
|
||||
for member in members:
|
||||
if member["member_id"] == portfolio.owner_role.id:
|
||||
ppoc = member
|
||||
members.remove(member)
|
||||
members.insert(0, ppoc)
|
||||
return members
|
||||
|
||||
return sorted(members_data, key=lambda member: member["user_name"])
|
||||
|
||||
|
||||
def render_admin_page(portfolio, form=None):
|
||||
pagination_opts = Paginator.get_pagination_opts(http_request)
|
||||
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
|
||||
members_data = get_members_data(portfolio)
|
||||
portfolio_form = PortfolioForm(data={"name": portfolio.name})
|
||||
member_perms_form = member_forms.MembersPermissionsForm(
|
||||
data={"members_permissions": members_data}
|
||||
)
|
||||
|
||||
portfolio_form = PortfolioForm(obj=portfolio)
|
||||
member_list = portfolio.members
|
||||
assign_ppoc_form = member_forms.AssignPPOCForm()
|
||||
|
||||
for pf_role in portfolio.roles:
|
||||
if pf_role.user != portfolio.owner and pf_role.is_active:
|
||||
assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)]
|
||||
@ -87,13 +75,12 @@ def render_admin_page(portfolio, form=None):
|
||||
"portfolios/admin.html",
|
||||
form=form,
|
||||
portfolio_form=portfolio_form,
|
||||
member_perms_form=member_perms_form,
|
||||
member_form=member_forms.NewForm(),
|
||||
members=filter_members_data(member_list, portfolio),
|
||||
new_manager_form=member_forms.NewForm(),
|
||||
assign_ppoc_form=assign_ppoc_form,
|
||||
portfolio=portfolio,
|
||||
audit_events=audit_events,
|
||||
user=g.current_user,
|
||||
ppoc_id=members_data[0].get("member_id"),
|
||||
current_member_id=current_member_id,
|
||||
applications_count=len(portfolio.applications),
|
||||
)
|
||||
@ -106,34 +93,6 @@ def admin(portfolio_id):
|
||||
return render_admin_page(portfolio)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/admin", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_USERS, message="view portfolio admin page")
|
||||
def edit_members(portfolio_id):
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
member_perms_form = member_forms.MembersPermissionsForm(http_request.form)
|
||||
|
||||
if member_perms_form.validate():
|
||||
for subform in member_perms_form.members_permissions:
|
||||
member_id = subform.member_id.data
|
||||
member = PortfolioRoles.get_by_id(member_id)
|
||||
if member is not portfolio.owner_role:
|
||||
new_perm_set = subform.data["permission_sets"]
|
||||
PortfolioRoles.update(member, new_perm_set)
|
||||
|
||||
flash("update_portfolio_members", portfolio=portfolio)
|
||||
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.admin",
|
||||
portfolio_id=portfolio_id,
|
||||
fragment="portfolio-members",
|
||||
_anchor="portfolio-members",
|
||||
)
|
||||
)
|
||||
else:
|
||||
return render_admin_page(portfolio)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
|
||||
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
|
||||
def update_ppoc(portfolio_id):
|
||||
|
@ -56,13 +56,3 @@ def reports(portfolio_id):
|
||||
monthly_spending=Reports.monthly_spending(portfolio),
|
||||
retrieved=datetime.now(), # mocked datetime of reporting data retrival
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/destroy", methods=["POST"])
|
||||
@user_can(Permissions.ARCHIVE_PORTFOLIO, message="archive portfolio")
|
||||
def delete_portfolio(portfolio_id):
|
||||
Portfolios.delete(portfolio=g.portfolio)
|
||||
|
||||
flash("portfolio_deleted", portfolio_name=g.portfolio.name)
|
||||
|
||||
return redirect(url_for("atst.home"))
|
||||
|
@ -79,7 +79,7 @@ def invite_member(portfolio_id):
|
||||
|
||||
if form.validate():
|
||||
try:
|
||||
invite = Portfolios.invite(portfolio, g.current_user, form.update_data)
|
||||
invite = Portfolios.invite(portfolio, g.current_user, form.data)
|
||||
send_portfolio_invitation(
|
||||
invitee_email=invite.email,
|
||||
inviter_name=g.current_user.full_name,
|
||||
|
@ -8,16 +8,16 @@ from atst.forms.task_order import SignatureForm
|
||||
from atst.models import Permissions
|
||||
|
||||
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>/review")
|
||||
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="review task order details")
|
||||
def review_task_order(task_order_id):
|
||||
@task_orders_bp.route("/task_orders/<task_order_id>")
|
||||
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order details")
|
||||
def view_task_order(task_order_id):
|
||||
task_order = TaskOrders.get(task_order_id)
|
||||
if task_order.is_draft:
|
||||
return redirect(url_for("task_orders.edit", task_order_id=task_order.id))
|
||||
else:
|
||||
signature_form = SignatureForm()
|
||||
return render_template(
|
||||
"task_orders/review.html",
|
||||
"task_orders/view.html",
|
||||
task_order=task_order,
|
||||
signature_form=signature_form,
|
||||
)
|
||||
|
@ -1,5 +1,10 @@
|
||||
import re
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from atst.database import db
|
||||
from atst.domain.exceptions import AlreadyExistsError
|
||||
|
||||
|
||||
def first_or_none(predicate, lst):
|
||||
return next((x for x in lst if predicate(x)), None)
|
||||
@ -23,3 +28,11 @@ def camel_to_snake(camel_cased):
|
||||
def pick(keys, dct):
|
||||
_keys = set(keys)
|
||||
return {k: v for (k, v) in dct.items() if k in _keys}
|
||||
|
||||
|
||||
def commit_or_raise_already_exists_error(message):
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise AlreadyExistsError(message)
|
||||
|
@ -1,11 +0,0 @@
|
||||
import pendulum
|
||||
|
||||
|
||||
class Clock(object):
|
||||
@classmethod
|
||||
def today(cls, tz="UTC"):
|
||||
return pendulum.today(tz=tz).date()
|
||||
|
||||
@classmethod
|
||||
def now(cls, tz="UTC"):
|
||||
return pendulum.now(tz=tz)
|
@ -1,204 +1,171 @@
|
||||
from flask import flash, render_template_string
|
||||
from flask import flash
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
MESSAGES = {
|
||||
"portfolio_deleted": {
|
||||
"title_template": "Portfolio has been deleted",
|
||||
"message_template": "Portfolio '{{portfolio_name}}' has been deleted",
|
||||
"category": "success",
|
||||
},
|
||||
"application_created": {
|
||||
"title_template": translate("flash.application.created.title"),
|
||||
"message_template": """
|
||||
{{ "flash.application.created.message" | translate({"application_name": application_name}) }}
|
||||
""",
|
||||
"title": "flash.application.created.title",
|
||||
"message": "flash.application.created.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_updated": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
{{ "flash.application.updated" | translate({"application_name": application_name}) }}
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.application.updated",
|
||||
"category": "success",
|
||||
},
|
||||
"application_deleted": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
{{ "flash.application.deleted" | translate({"application_name": application_name}) }}
|
||||
<a href="#">{{ "common.undo" | translate }}</a>
|
||||
""",
|
||||
"category": "success",
|
||||
"application_environments_name_error": {
|
||||
"title": None,
|
||||
"message": "flash.application.env_name_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_environments_updated": {
|
||||
"title_template": "Application environments updated",
|
||||
"message_template": "Application environments have been updated",
|
||||
"title": "flash.environment.updated.title",
|
||||
"message": "flash.environment.updated.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_invite_error": {
|
||||
"title_template": "Application invitation error",
|
||||
"message_template": "There was an error processing the invitation for {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_invite.error.title",
|
||||
"message": "flash.application_invite.error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_invite_resent": {
|
||||
"title_template": "Application invitation resent",
|
||||
"message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_invite.resent.title",
|
||||
"message": "flash.application_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_invite_revoked": {
|
||||
"title_template": "Application invitation revoked",
|
||||
"message_template": "You have successfully revoked the invite for {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_invite.revoked.title",
|
||||
"message": "flash.application_invite.revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_removed": {
|
||||
"title_template": "Team member removed from application",
|
||||
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
|
||||
"title": "flash.application_member.removed.title",
|
||||
"message": "flash.application_member.removed.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_member_update_error": {
|
||||
"title_template": "{{ user_name }} could not be updated",
|
||||
"message_template": "An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.",
|
||||
"title": "flash.application_member.update_error.title",
|
||||
"message": "flash.application_member.update_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"application_member_updated": {
|
||||
"title_template": "Team member updated",
|
||||
"message_template": "You have successfully updated the permissions for {{ user_name }}",
|
||||
"title": "flash.application_member.updated.title",
|
||||
"message": "flash.application_member.updated.message",
|
||||
"category": "success",
|
||||
},
|
||||
"application_name_error": {
|
||||
"title": None,
|
||||
"message": "flash.application.name_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"ccpo_user_added": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": "You have successfully given {{ user_name }} CCPO permissions.",
|
||||
"title": "flash.success",
|
||||
"message": "flash.ccpo_user.added.message",
|
||||
"category": "success",
|
||||
},
|
||||
"ccpo_user_not_found": {
|
||||
"title_template": translate("ccpo.form.user_not_found_title"),
|
||||
"message_template": translate("ccpo.form.user_not_found_text"),
|
||||
"title": "ccpo.form.user_not_found_title",
|
||||
"message": "ccpo.form.user_not_found_text",
|
||||
"category": "info",
|
||||
},
|
||||
"ccpo_user_removed": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": "You have successfully removed {{ user_name }}'s CCPO permissions.",
|
||||
"title": "flash.success",
|
||||
"message": "flash.ccpo_user.removed.message",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_added": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
{{ "flash.environment_added" | translate({ "env_name": environment_name }) }}
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.environment_added",
|
||||
"category": "success",
|
||||
},
|
||||
"environment_deleted": {
|
||||
"title_template": "{{ environment_name }} deleted",
|
||||
"message_template": 'The environment "{{ environment_name }}" has been deleted',
|
||||
"title": "flash.environment.deleted.title",
|
||||
"message": "flash.environment.deleted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"form_errors": {
|
||||
"title_template": "There were some errors",
|
||||
"message_template": "<p>Please see below.</p>",
|
||||
"title": "flash.form.errors.title",
|
||||
"message": "flash.form.errors.message",
|
||||
"category": "error",
|
||||
},
|
||||
"insufficient_funds": {
|
||||
"title_template": "Insufficient Funds",
|
||||
"message_template": "",
|
||||
"title": "flash.task_order.insufficient_funds.title",
|
||||
"message": "",
|
||||
"category": "warning",
|
||||
},
|
||||
"logged_out": {
|
||||
"title_template": translate("flash.logged_out"),
|
||||
"message_template": """
|
||||
You've been logged out.
|
||||
""",
|
||||
"title": "flash.logged_out.title",
|
||||
"message": "flash.logged_out.message",
|
||||
"category": "info",
|
||||
},
|
||||
"login_next": {
|
||||
"title_template": translate("flash.login_required_title"),
|
||||
"message_template": translate("flash.login_required_message"),
|
||||
"title": "flash.login_required_title",
|
||||
"message": "flash.login_required_message",
|
||||
"category": "warning",
|
||||
},
|
||||
"new_application_member": {
|
||||
"title_template": """{{ "flash.new_application_member.title" | translate({ "user_name": user_name }) }}""",
|
||||
"message_template": """
|
||||
<p>{{ "flash.new_application_member.message" | translate({ "user_name": user_name }) }}</p>
|
||||
""",
|
||||
"title": "flash.new_application_member.title",
|
||||
"message": "flash.new_application_member.message",
|
||||
"category": "success",
|
||||
},
|
||||
"new_portfolio_member": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
<p>{{ "flash.new_portfolio_member" | translate({ "user_name": user_name }) }}</p>
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.new_portfolio_member",
|
||||
"category": "success",
|
||||
},
|
||||
"portfolio_member_removed": {
|
||||
"title_template": translate("flash.deleted_member"),
|
||||
"message_template": """
|
||||
{{ "flash.delete_member_success" | translate({ "member_name": member_name }) }}
|
||||
""",
|
||||
"title": "flash.deleted_member",
|
||||
"message": "flash.delete_member_success",
|
||||
"category": "success",
|
||||
},
|
||||
"primary_point_of_contact_changed": {
|
||||
"title_template": translate("flash.new_ppoc_title"),
|
||||
"message_template": """{{ "flash.new_ppoc_message" | translate({ "ppoc_name": ppoc_name }) }}""",
|
||||
"title": "flash.new_ppoc_title",
|
||||
"message": "flash.new_ppoc_message",
|
||||
"category": "success",
|
||||
},
|
||||
"resend_portfolio_invitation": {
|
||||
"title_template": "Invitation resent",
|
||||
"message_template": """
|
||||
<p>Successfully sent a new invitation to {{ user_name }}.</p>
|
||||
""",
|
||||
"title": "flash.portfolio_invite.resent.title",
|
||||
"message": "flash.portfolio_invite.resent.message",
|
||||
"category": "success",
|
||||
},
|
||||
"revoked_portfolio_access": {
|
||||
"title_template": "Removed portfolio access",
|
||||
"message_template": """
|
||||
<p>Portfolio access successfully removed from {{ member_name }}.</p>
|
||||
""",
|
||||
"title": "flash.portfolio_member.revoked.title",
|
||||
"message": "flash.portfolio_member.revoked.message",
|
||||
"category": "success",
|
||||
},
|
||||
"session_expired": {
|
||||
"title_template": "Session Expired",
|
||||
"message_template": """
|
||||
Your session expired due to inactivity. Please log in again to continue.
|
||||
""",
|
||||
"title": "flash.session_expired.title",
|
||||
"message": "flash.session_expired.message",
|
||||
"category": "error",
|
||||
},
|
||||
"task_order_draft": {
|
||||
"title_template": translate("task_orders.form.draft_alert_title"),
|
||||
"message_template": translate("task_orders.form.draft_alert_message"),
|
||||
"title": "task_orders.form.draft_alert_title",
|
||||
"message": "task_orders.form.draft_alert_message",
|
||||
"category": "warning",
|
||||
},
|
||||
"task_order_number_error": {
|
||||
"title_template": "",
|
||||
"message_template": """{{ 'flash.task_order_number_error.message' | translate({ 'to_number': to_number }) }}""",
|
||||
"title": None,
|
||||
"message": "flash.task_order_number_error.message",
|
||||
"category": "error",
|
||||
},
|
||||
"task_order_submitted": {
|
||||
"title_template": "Your Task Order has been uploaded successfully.",
|
||||
"message_template": """
|
||||
Your task order form for {{ task_order.portfolio_name }} has been submitted.
|
||||
""",
|
||||
"category": "success",
|
||||
},
|
||||
"update_portfolio_members": {
|
||||
"title_template": "Success!",
|
||||
"message_template": """
|
||||
<p>You have successfully updated access permissions for members of {{ portfolio.name }}.</p>
|
||||
""",
|
||||
"title": "flash.task_order.submitted.title",
|
||||
"message": "flash.task_order.submitted.message",
|
||||
"category": "success",
|
||||
},
|
||||
"updated_application_team_settings": {
|
||||
"title_template": translate("flash.success"),
|
||||
"message_template": """
|
||||
<p>{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}</p>
|
||||
""",
|
||||
"title": "flash.success",
|
||||
"message": "flash.updated_application_team_settings",
|
||||
"category": "success",
|
||||
},
|
||||
"user_must_complete_profile": {
|
||||
"title_template": "You must complete your profile",
|
||||
"message_template": "<p>Before continuing, you must complete your profile</p>",
|
||||
"title": "flash.user.complete_profile.title",
|
||||
"message": "flash.user.complete_profile.message",
|
||||
"category": "info",
|
||||
},
|
||||
"user_updated": {
|
||||
"title_template": "User information updated.",
|
||||
"message_template": "",
|
||||
"title": "flash.user.updated.title",
|
||||
"message": None,
|
||||
"category": "success",
|
||||
},
|
||||
}
|
||||
@ -206,9 +173,11 @@ MESSAGES = {
|
||||
|
||||
def formatted_flash(message_name, **message_args):
|
||||
config = MESSAGES[message_name]
|
||||
title = render_template_string(config["title_template"], **message_args)
|
||||
message = render_template_string(config["message_template"], **message_args)
|
||||
actions = None
|
||||
if "actions" in config:
|
||||
actions = render_template_string(config["actions"], **message_args)
|
||||
|
||||
title = translate(config["title"], message_args) if config["title"] else None
|
||||
message = translate(config["message"], message_args) if config["message"] else None
|
||||
actions = (
|
||||
translate(config["actions"], message_args) if config.get("actions") else None
|
||||
)
|
||||
|
||||
flash({"title": title, "message": message, "actions": actions}, config["category"])
|
||||
|
@ -2,16 +2,22 @@ import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
from flask import g, request, has_request_context
|
||||
from flask import g, request, has_request_context, session
|
||||
|
||||
|
||||
class RequestContextFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
if has_request_context():
|
||||
if getattr(g, "current_user", None):
|
||||
record.user_id = str(g.current_user.id)
|
||||
record.dod_edipi = g.current_user.dod_id
|
||||
|
||||
user_id = session.get("user_id")
|
||||
if user_id:
|
||||
record.user_id = str(user_id)
|
||||
record.logged_in = True
|
||||
else:
|
||||
record.logged_in = False
|
||||
|
||||
if request.environ.get("HTTP_X_REQUEST_ID"):
|
||||
record.request_id = request.environ.get("HTTP_X_REQUEST_ID")
|
||||
|
||||
@ -30,6 +36,7 @@ class JsonFormatter(logging.Formatter):
|
||||
("request_id", lambda r: r.__dict__.get("request_id")),
|
||||
("user_id", lambda r: r.__dict__.get("user_id")),
|
||||
("dod_edipi", lambda r: r.__dict__.get("dod_edipi")),
|
||||
("logged_in", lambda r: r.__dict__.get("logged_in")),
|
||||
("severity", lambda r: r.levelname),
|
||||
("tags", lambda r: r.__dict__.get("tags")),
|
||||
("audit_event", lambda r: r.__dict__.get("audit_event")),
|
||||
@ -44,7 +51,7 @@ class JsonFormatter(logging.Formatter):
|
||||
|
||||
for field, func in self._DEFAULT_RECORD_FIELDS:
|
||||
result = func(record)
|
||||
if result:
|
||||
if result is not None:
|
||||
message_dict[field] = result
|
||||
|
||||
if record.args:
|
||||
|
@ -3,6 +3,7 @@ ASSETS_URL
|
||||
AZURE_ACCOUNT_NAME
|
||||
AZURE_STORAGE_KEY
|
||||
AZURE_TO_BUCKET_NAME
|
||||
AZURE_POLICY_LOCATION=policies
|
||||
BLOB_STORAGE_URL=http://localhost:8000/
|
||||
CAC_URL = http://localhost:8000/login-redirect
|
||||
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
||||
@ -39,6 +40,7 @@ REDIS_USER
|
||||
SECRET_KEY = change_me_into_something_secret
|
||||
SERVER_NAME
|
||||
SESSION_COOKIE_NAME=atat
|
||||
SESSION_COOKIE_DOMAIN
|
||||
SESSION_TYPE = redis
|
||||
SESSION_USE_SIGNER = True
|
||||
SQLALCHEMY_ECHO = False
|
||||
|
@ -30,6 +30,7 @@ data:
|
||||
PGUSER: atat_master@atat-db
|
||||
REDIS_HOST: atat.redis.cache.windows.net:6380
|
||||
REDIS_TLS: "true"
|
||||
SESSION_COOKIE_DOMAIN: atat.code.mil
|
||||
STATIC_URL: https://atat-cdn.azureedge.net/static/
|
||||
TZ: UTC
|
||||
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
||||
|
@ -45,7 +45,7 @@ data:
|
||||
include /etc/nginx/snippets/ssl.conf;
|
||||
|
||||
location /login-redirect {
|
||||
return 301 https://auth-azure.atat.code.mil$request_uri;
|
||||
return 301 https://${AUTH_DOMAIN}$request_uri;
|
||||
}
|
||||
location /login-dev {
|
||||
try_files $uri @appbasicauth;
|
||||
@ -82,7 +82,7 @@ data:
|
||||
include /etc/nginx/snippets/ssl.conf;
|
||||
|
||||
location / {
|
||||
return 301 https://azure.atat.code.mil$request_uri;
|
||||
return 301 https://${MAIN_DOMAIN}$request_uri;
|
||||
}
|
||||
location /login-redirect {
|
||||
try_files $uri @app;
|
||||
|
22
deploy/overlays/cloudzero-dev/envvars.yml
Normal file
22
deploy/overlays/cloudzero-dev/envvars.yml
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-worker-envvars
|
||||
data:
|
||||
CELERY_DEFAULT_QUEUE: celery-staging
|
||||
SERVER_NAME: staging.atat.code.mil
|
||||
FLASK_ENV: staging
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: atst-envvars
|
||||
data:
|
||||
ASSETS_URL: https://atat-cdn-staging.azureedge.net/
|
||||
CDN_ORIGIN: https://staging.atat.code.mil
|
||||
CELERY_DEFAULT_QUEUE: celery-staging
|
||||
FLASK_ENV: staging
|
||||
STATIC_URL: https://atat-cdn-staging.azureedge.net/static/
|
||||
PGHOST: cloudzero-dev-sql.postgres.database.azure.com
|
||||
REDIS_HOST: cloudzero-dev-redis.redis.cache.windows.net:6380
|
62
deploy/overlays/cloudzero-dev/flex_vol.yml
Normal file
62
deploy/overlays/cloudzero-dev/flex_vol.yml
Normal file
@ -0,0 +1,62 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
volumes:
|
||||
- name: nginx-secret
|
||||
flexVolume:
|
||||
options:
|
||||
keyvaultname: "atat-vault-test"
|
||||
keyvaultobjectnames: "dhparam4096;cert;cert"
|
||||
- name: flask-secret
|
||||
flexVolume:
|
||||
options:
|
||||
keyvaultname: "atat-vault-test"
|
||||
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst-worker
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
volumes:
|
||||
- name: flask-secret
|
||||
flexVolume:
|
||||
options:
|
||||
keyvaultname: "atat-vault-test"
|
||||
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst-beat
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
volumes:
|
||||
- name: flask-secret
|
||||
flexVolume:
|
||||
options:
|
||||
keyvaultname: "atat-vault-test"
|
||||
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"
|
||||
---
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: crls
|
||||
spec:
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
volumes:
|
||||
- name: flask-secret
|
||||
flexVolume:
|
||||
options:
|
||||
keyvaultname: "atat-vault-test"
|
||||
keyvaultobjectnames: "AZURE-STORAGE-KEY;MAIL-PASSWORD;PGPASSWORD;REDIS-PASSWORD;SECRET-KEY"
|
12
deploy/overlays/cloudzero-dev/json_ports.yml
Normal file
12
deploy/overlays/cloudzero-dev/json_ports.yml
Normal file
@ -0,0 +1,12 @@
|
||||
- op: replace
|
||||
path: /spec/template/spec/containers/1/ports/0/containerPort
|
||||
value: 9342
|
||||
- op: replace
|
||||
path: /spec/template/spec/containers/1/ports/1/containerPort
|
||||
value: 9442
|
||||
- op: replace
|
||||
path: /spec/template/spec/containers/1/ports/2/containerPort
|
||||
value: 9343
|
||||
- op: replace
|
||||
path: /spec/template/spec/containers/1/ports/3/containerPort
|
||||
value: 9443
|
18
deploy/overlays/cloudzero-dev/kustomization.yaml
Normal file
18
deploy/overlays/cloudzero-dev/kustomization.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
namespace: staging
|
||||
bases:
|
||||
- ../../azure/
|
||||
resources:
|
||||
- namespace.yml
|
||||
- reset-cron-job.yml
|
||||
patchesStrategicMerge:
|
||||
- replica_count.yml
|
||||
- ports.yml
|
||||
- envvars.yml
|
||||
- flex_vol.yml
|
||||
patchesJson6902:
|
||||
- target:
|
||||
group: extensions
|
||||
version: v1beta1
|
||||
kind: Deployment
|
||||
name: atst
|
||||
path: json_ports.yml
|
4
deploy/overlays/cloudzero-dev/namespace.yml
Normal file
4
deploy/overlays/cloudzero-dev/namespace.yml
Normal file
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: staging
|
28
deploy/overlays/cloudzero-dev/ports.yml
Normal file
28
deploy/overlays/cloudzero-dev/ports.yml
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: atst-main
|
||||
spec:
|
||||
loadBalancerIP: ""
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 9342
|
||||
name: http
|
||||
- port: 443
|
||||
targetPort: 9442
|
||||
name: https
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: atst-auth
|
||||
spec:
|
||||
loadBalancerIP: ""
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 9343
|
||||
name: http
|
||||
- port: 443
|
||||
targetPort: 9443
|
||||
name: https
|
14
deploy/overlays/cloudzero-dev/replica_count.yml
Normal file
14
deploy/overlays/cloudzero-dev/replica_count.yml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst
|
||||
spec:
|
||||
replicas: 2
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: atst-worker
|
||||
spec:
|
||||
replicas: 1
|
46
deploy/overlays/cloudzero-dev/reset-cron-job.yml
Normal file
46
deploy/overlays/cloudzero-dev/reset-cron-job.yml
Normal file
@ -0,0 +1,46 @@
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: reset-db
|
||||
namespace: atat
|
||||
spec:
|
||||
schedule: "0 4 * * *"
|
||||
concurrencyPolicy: Replace
|
||||
successfulJobsHistoryLimit: 1
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
role: reset-db
|
||||
aadpodidbinding: atat-kv-id-binding
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: reset
|
||||
image: $CONTAINER_IMAGE
|
||||
command: [
|
||||
"/bin/sh", "-c"
|
||||
]
|
||||
args: [
|
||||
"/opt/atat/atst/.venv/bin/python",
|
||||
"/opt/atat/atst/script/reset_database.py"
|
||||
]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: atst-worker-envvars
|
||||
volumeMounts:
|
||||
- name: flask-secret
|
||||
mountPath: "/config"
|
||||
volumes:
|
||||
- name: flask-secret
|
||||
flexVolume:
|
||||
driver: "azure/kv"
|
||||
options:
|
||||
usepodidentity: "true"
|
||||
keyvaultname: "atat-vault-test"
|
||||
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
|
||||
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
|
||||
keyvaultobjecttypes: "secret;secret;secret;secret;key"
|
||||
tenantid: $TENANT_ID
|
@ -3,6 +3,7 @@ bases:
|
||||
- ../../azure/
|
||||
resources:
|
||||
- namespace.yml
|
||||
- reset-cron-job.yml
|
||||
patchesStrategicMerge:
|
||||
- replica_count.yml
|
||||
- ports.yml
|
||||
|
46
deploy/overlays/staging/reset-cron-job.yml
Normal file
46
deploy/overlays/staging/reset-cron-job.yml
Normal file
@ -0,0 +1,46 @@
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: reset-db
|
||||
namespace: atat
|
||||
spec:
|
||||
schedule: "0 4 * * *"
|
||||
concurrencyPolicy: Replace
|
||||
successfulJobsHistoryLimit: 1
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: atst
|
||||
role: reset-db
|
||||
aadpodidbinding: atat-kv-id-binding
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: reset
|
||||
image: $CONTAINER_IMAGE
|
||||
command: [
|
||||
"/bin/sh", "-c"
|
||||
]
|
||||
args: [
|
||||
"/opt/atat/atst/.venv/bin/python",
|
||||
"/opt/atat/atst/script/reset_database.py"
|
||||
]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: atst-worker-envvars
|
||||
volumeMounts:
|
||||
- name: flask-secret
|
||||
mountPath: "/config"
|
||||
volumes:
|
||||
- name: flask-secret
|
||||
flexVolume:
|
||||
driver: "azure/kv"
|
||||
options:
|
||||
usepodidentity: "true"
|
||||
keyvaultname: "atat-vault-test"
|
||||
keyvaultobjectnames: "staging-AZURE-STORAGE-KEY;staging-MAIL-PASSWORD;staging-PGPASSWORD;staging-REDIS-PASSWORD;staging-SECRET-KEY"
|
||||
keyvaultobjectaliases: "AZURE_STORAGE_KEY;MAIL_PASSWORD;PGPASSWORD;REDIS_PASSWORD;SECRET_KEY"
|
||||
keyvaultobjecttypes: "secret;secret;secret;secret;key"
|
||||
tenantid: $TENANT_ID
|
@ -1,4 +1,5 @@
|
||||
import { emitFieldChange } from '../lib/emitters'
|
||||
import escape from '../lib/escape'
|
||||
import optionsinput from './options_input'
|
||||
import textinput from './text_input'
|
||||
import clindollaramount from './clin_dollar_amount'
|
||||
@ -99,7 +100,7 @@ export default {
|
||||
computed: {
|
||||
clinTitle: function() {
|
||||
if (!!this.clinNumber) {
|
||||
return `CLIN ${this.clinNumber}`
|
||||
return escape(`CLIN ${this.clinNumber}`)
|
||||
} else {
|
||||
return `CLIN`
|
||||
}
|
||||
|
21
js/lib/__tests__/escape.test.js
Normal file
21
js/lib/__tests__/escape.test.js
Normal file
@ -0,0 +1,21 @@
|
||||
import escape from '../escape'
|
||||
describe('escape', () => {
|
||||
const htmlEscapes = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
}
|
||||
it('should escape each character', () => {
|
||||
for (let [char, escapedChar] of Object.entries(htmlEscapes)) {
|
||||
expect(escape(char)).toBe(escapedChar)
|
||||
}
|
||||
})
|
||||
it('should escape multiple characters', () => {
|
||||
expect(escape('& and < and > and " and \' and /')).toBe(
|
||||
'& and < and > and " and ' and /'
|
||||
)
|
||||
})
|
||||
})
|
20
js/lib/escape.js
Normal file
20
js/lib/escape.js
Normal file
@ -0,0 +1,20 @@
|
||||
// https://stackoverflow.com/a/6020820
|
||||
|
||||
// List of HTML entities for escaping.
|
||||
const htmlEscapes = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
}
|
||||
|
||||
const htmlEscaper = /[&<>"'\/]/g
|
||||
|
||||
// Escape a string for HTML interpolation.
|
||||
const escape = string => {
|
||||
return ('' + string).replace(htmlEscaper, match => htmlEscapes[match])
|
||||
}
|
||||
|
||||
export default escape
|
40
policies/portfolios/allowed-resource-types.json
Normal file
40
policies/portfolios/allowed-resource-types.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"definitionPoint": "portfolio",
|
||||
"policyDefinition": {
|
||||
"properties": {
|
||||
"displayName": "Allowed resource types",
|
||||
"policyType": "Custom",
|
||||
"mode": "Indexed",
|
||||
"description": "This policy enables you to specify the resource types that your organization can deploy.",
|
||||
"parameters": {
|
||||
"listOfResourceTypesAllowed": {
|
||||
"type": "Array",
|
||||
"metadata": {
|
||||
"description": "The list of resource types that can be deployed.",
|
||||
"displayName": "Allowed resource types",
|
||||
"strongType": "resourceTypes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"not": {
|
||||
"field": "type",
|
||||
"in": "[parameters('listOfResourceTypesAllowed')]"
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"effect": "deny"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "Microsoft.Authorization/policyDefinitions"
|
||||
},
|
||||
"parameters": {
|
||||
"listOfResourceTypesAllowed": {
|
||||
"value": [
|
||||
"Microsoft.Cache"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
51
policies/portfolios/region-restriction.json
Normal file
51
policies/portfolios/region-restriction.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"definitionPoint": "portfolio",
|
||||
"policyDefinition": {
|
||||
"properties": {
|
||||
"displayName": "Custom - Region Restriction",
|
||||
"policyType": "Custom",
|
||||
"mode": "Indexed",
|
||||
"parameters": {
|
||||
"listOfAllowedLocations": {
|
||||
"type": "Array",
|
||||
"metadata": {
|
||||
"displayName": "Allowed locations",
|
||||
"description": "The list of locations that can be specified when deploying resources.",
|
||||
"strongType": "location"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"allOf": [
|
||||
{
|
||||
"field": "location",
|
||||
"notIn": "[parameters('listOfAllowedLocations')]"
|
||||
},
|
||||
{
|
||||
"field": "location",
|
||||
"notEquals": "global"
|
||||
},
|
||||
{
|
||||
"field": "type",
|
||||
"notEquals": "Microsoft.AzureActiveDirectory/b2cDirectories"
|
||||
}
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"effect": "Deny"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "Microsoft.Authorization/policyDefinitions"
|
||||
},
|
||||
"parameters": {
|
||||
"listOfAllowedLocations": {
|
||||
"value": [
|
||||
"eastus",
|
||||
"southcentralus",
|
||||
"westus"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# script/cibuild: Run CI related checks and tests
|
||||
|
||||
|
@ -26,28 +26,16 @@ fi
|
||||
|
||||
## Main
|
||||
if [ "${RUN_PYTHON_TESTS}" = "true" ]; then
|
||||
python_test_status=0
|
||||
set +e
|
||||
|
||||
output_divider "Lint Python files"
|
||||
run_python_lint "${PYTHON_FILES}"
|
||||
((python_test_status+=$?))
|
||||
output_divider "Perform static analysis on Python files"
|
||||
run_python_static_analysis "${PYTHON_FILES}"
|
||||
((python_test_status+=$?))
|
||||
output_divider "Perform type checking on Python files"
|
||||
run_python_typecheck
|
||||
((python_test_status+=$?))
|
||||
output_divider "Run Python unit test suite"
|
||||
run_python_unit_tests "${PYTHON_FILES}"
|
||||
((python_test_status+=$?))
|
||||
|
||||
if [ "${python_test_status}" != "0" ]; then
|
||||
warning "Failed to pass one or more Python checks"
|
||||
exit ${python_test_status}
|
||||
fi
|
||||
|
||||
set -e
|
||||
fi
|
||||
|
||||
if [ "${RUN_JS_TESTS}" = "true" ]; then
|
||||
|
@ -72,15 +72,18 @@ $CONTAINER_IMAGE \
|
||||
|
||||
# Use curl to wait for application container to become available
|
||||
docker pull curlimages/curl:latest
|
||||
echo "Waiting for application container to become available"
|
||||
docker run --network atat \
|
||||
curlimages/curl:latest \
|
||||
curl --connect-timeout 3 \
|
||||
curl \
|
||||
--silent \
|
||||
--connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry $CONTAINER_TIMEOUT \
|
||||
--retry-connrefused \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time $CONTAINER_TIMEOUT \
|
||||
test-atat:8000
|
||||
test-atat:8000 >/dev/null
|
||||
|
||||
# Run Ghost Inspector tests
|
||||
docker pull ghostinspector/test-runner-standalone:latest
|
||||
|
@ -34,7 +34,7 @@ from atst.routes.dev import _DEV_USERS as DEV_USERS
|
||||
from atst.utils import pick
|
||||
|
||||
from tests.factories import (
|
||||
random_service_branch,
|
||||
random_defense_component,
|
||||
TaskOrderFactory,
|
||||
CLINFactory,
|
||||
AttachmentFactory,
|
||||
@ -159,7 +159,7 @@ def get_users():
|
||||
|
||||
def add_members_to_portfolio(portfolio):
|
||||
for user_data in PORTFOLIO_USERS:
|
||||
invite = Portfolios.invite(portfolio, portfolio.owner, user_data)
|
||||
invite = Portfolios.invite(portfolio, portfolio.owner, {"user_data": user_data})
|
||||
profile = {
|
||||
k: user_data[k] for k in user_data if k not in ["dod_id", "permission_sets"]
|
||||
}
|
||||
@ -308,7 +308,7 @@ def create_demo_portfolio(name, data):
|
||||
|
||||
portfolio = Portfolios.create(
|
||||
user=portfolio_owner,
|
||||
portfolio_attrs={"name": name, "defense_component": random_service_branch()},
|
||||
portfolio_attrs={"name": name, "defense_component": random_defense_component()},
|
||||
)
|
||||
|
||||
add_task_orders_to_portfolio(portfolio)
|
||||
@ -336,7 +336,7 @@ def seed_db():
|
||||
user=amanda,
|
||||
portfolio_attrs={
|
||||
"name": "TIE Interceptor",
|
||||
"defense_component": random_service_branch(),
|
||||
"defense_component": random_defense_component(),
|
||||
},
|
||||
)
|
||||
add_task_orders_to_portfolio(tie_interceptor)
|
||||
@ -347,7 +347,7 @@ def seed_db():
|
||||
user=amanda,
|
||||
portfolio_attrs={
|
||||
"name": "TIE Fighter",
|
||||
"defense_component": random_service_branch(),
|
||||
"defense_component": random_defense_component(),
|
||||
},
|
||||
)
|
||||
add_task_orders_to_portfolio(tie_fighter)
|
||||
@ -363,7 +363,7 @@ def seed_db():
|
||||
user=user,
|
||||
portfolio_attrs={
|
||||
"name": ship,
|
||||
"defense_component": random_service_branch(),
|
||||
"defense_component": random_defense_component(),
|
||||
},
|
||||
)
|
||||
add_task_orders_to_portfolio(portfolio)
|
||||
|
@ -1,25 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEJDCCAwygAwIBAgIJAK4JGo3BBGhVMA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNV
|
||||
BAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExFTATBgNVBAcTDFBoaWxhZGVs
|
||||
cGhpYTEMMAoGA1UEChMDRG9EMQwwCgYDVQQLEwNERFMxEDAOBgNVBAMTB0FUQVQg
|
||||
Q0EwHhcNMTgwNjAxMTk0NjIyWhcNMzgwNTI3MTk0NjIyWjBpMQswCQYDVQQGEwJV
|
||||
UzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRUwEwYDVQQHEwxQaGlsYWRlbHBoaWEx
|
||||
DDAKBgNVBAoTA0RvRDEMMAoGA1UECxMDRERTMRAwDgYDVQQDEwdBVEFUIENBMIIB
|
||||
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzYU7UbstArnnVliaC/TB6Vir
|
||||
kVWMnAEYMUZA1BKP8DZaNEKbzFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo
|
||||
13i5EwUUCSh2MdPfS8ZZt8DUIIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8
|
||||
VZkFEgvs8FCP0M4Ar6/gtJ24ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbH
|
||||
LkOM2gtp/pkYCCG0zqeU+0s3H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibk
|
||||
aI6sTTooXE5aSZkfkx0z6+fKM2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQID
|
||||
AQABo4HOMIHLMB0GA1UdDgQWBBSl7CUAWPbx8XqotKKKAufPh0wn4DCBmwYDVR0j
|
||||
BIGTMIGQgBSl7CUAWPbx8XqotKKKAufPh0wn4KFtpGswaTELMAkGA1UEBhMCVVMx
|
||||
FTATBgNVBAgTDFBlbm5zeWx2YW5pYTEVMBMGA1UEBxMMUGhpbGFkZWxwaGlhMQww
|
||||
CgYDVQQKEwNEb0QxDDAKBgNVBAsTA0REUzEQMA4GA1UEAxMHQVRBVCBDQYIJAK4J
|
||||
Go3BBGhVMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABguwdFk42YP
|
||||
8U6Du5HQ6Is1jfc1KEOowdh0d2MCH8q0KNktqiu6kWzjH1gRjRwc07bAkAWqXPB6
|
||||
6gkRGYe/FRgi2Rn+Uo5UC5ahI4cXkE8OitCIEP3Br9fUw+vj/3Iiov0QZ6Hv81Kl
|
||||
ZTZhLiZbjAg5maL/vufnUp+n15qzm67APh3/2hcgO93UlE9o9vXohWy1lHs8u12o
|
||||
hPLxghSmGc9eKalEWEs61OrohpOtCHUEd1isq76WhaiXSwSUrBxgy89Z517A7ffC
|
||||
BjzLo5AVo6a9ou+ONVeZk8qw6YR6X9J7axy8YuTWt+Z82WFvOF0ubkqjm72d001M
|
||||
7R9zCOQ3O+g=
|
||||
-----END CERTIFICATE-----
|
@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAzYU7UbstArnnVliaC/TB6VirkVWMnAEYMUZA1BKP8DZaNEKb
|
||||
zFH2+mMw7O0BY7Ph9x0hEZ1kXLr6U93xcKyUWNPo13i5EwUUCSh2MdPfS8ZZt8DU
|
||||
IIKC7XzFnKyKSKQmr0Mt9dC44rryPKTBvmI60rQ8VZkFEgvs8FCP0M4Ar6/gtJ24
|
||||
ZLEtilu5dQBSlru4nPGXg07r2C2JgEZWshtMBtbHLkOM2gtp/pkYCCG0zqeU+0s3
|
||||
H8IqDq0uYkONOfVeCumbg1/AtjgrZu7aOVPKyibkaI6sTTooXE5aSZkfkx0z6+fK
|
||||
M2nPSe30HgiBODtb7G+44ln08d0isjpQ67OvGQIDAQABAoIBAHR4EInc3UEyQVu5
|
||||
knM8Hbgzu+b86FZweFlUSuDkNBYZdz0ukkRUHvb+x3c9SRBLnL8CDv+AhqPWgo6M
|
||||
tIr6Aofkb4vMqnWQ5y3ZdEIApAa5PZbY/F4AGFql3wdO8H8CJ7ojBCTOSDiVYTnk
|
||||
1Lcjy9okshyAP1Ne1sPJo/bdB56HtXs+wqok1NntIQwiXjjD9xUuc1EZk0J4M97L
|
||||
vBUjUGNX942UjtRiey5zwhRp3bTPasTduHcA01NaIbOVYlRFwc2W+cflz0l6ml2p
|
||||
14TNEEvIMMMCNKnlPrpGI23n0psAvE4nbuxZQGVYAFvXrWn+Gyvz0Yag2EoMUCEs
|
||||
ziLED9ECgYEA6IByu+xqIuIAhj/PwIIxV4+lkuV4TXIlfAFLR4JuokOVfbRsmu2e
|
||||
9EfeOUD9LfQ4KsG5mu4Abpja0k/VKRKRGRjV6Oe2C6VK942HFP6Kpn0hgIuomZkD
|
||||
eVv8naDezZjAvVace38zjRWB2GXTpapwBAgf/YflPPsDZ8bi/weqZCMCgYEA4kqx
|
||||
Ka489Rr7+cSXpMeS5lLufhlaE5OVQc5HVFREDAI5vXU8BM2sLiHTC/BHjis2JvLm
|
||||
aRJ0UsxUoIUURl2KjTbx3zns4HDVkzBrSpoDXWxBjAo0oEg7JVc+6+qEqbDHHS1L
|
||||
/UJ6mlUegsE42MkFWG3YJQuHxyLZqPXIwNAyhZMCgYEA5cxnGnSt5rJoAEi7xzMn
|
||||
H7s71Hf3stw6TlldFV3GiZyw+aDFo09vR1RtQTuJwczbYu88yvOn+6gax7neHo1a
|
||||
WmrgqiWzGcmS0iDRPZ/kXG/bGBlxV/cTpvSTNx0UejMbdUhQvANaaXyzbLYgPWK6
|
||||
+lEphUW2/tG+aOj73UOvVu8CgYA5L8sJz4CUKJeZDTeNauoSzs56i4mZ/OfxU2Hv
|
||||
S8ROjJlu6ZubUya6Gc4t7DEJGp56xVO5JfLDoeOZFUiEZ8tF2KbTVN4p8hnnMotK
|
||||
tRU4nM0LyOB3yQk5bIz4LbIM+CG5m+LiQ9Sb//rP7GijUFnLeSbwZbOQfZwn+MUd
|
||||
BQBfhQKBgQDmuX8tJdPkjE133IhQhZHbHHt6AEQA3aXkFdvPvbYD9VbGTZ8wnpFO
|
||||
VJrDDWnIKAgO2FerIX9oq+H9a5fggYtTMeAX1cOA6b9SnLmFjt0utxrQKxf7p5I+
|
||||
n+EsmcAWfb+KRQwoB0L/mE9Ool14AeJ15kHyNIrCrMPv0J4zoC0Jdg==
|
||||
-----END RSA PRIVATE KEY-----
|
@ -1 +0,0 @@
|
||||
F4D74F1607DD3C83
|
@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Generate the root (GIVE IT A PASSWORD IF YOU'RE NOT AUTOMATING SIGNING!):
|
||||
echo 'MAKING CA'
|
||||
openssl genrsa -out certificate-authority/ca.key 2048
|
||||
openssl req -new -x509 -days 7300 -key certificate-authority/ca.key -sha256 -extensions v3_ca -out certificate-authority/ca.crt
|
||||
|
||||
# Generate the domain key:
|
||||
openssl genrsa -out server-certs/dev.cac.atat.codes.key 2048
|
||||
|
||||
echo 'MAKING CSR'
|
||||
# Generate the certificate signing request
|
||||
openssl req -nodes -sha256 -new -key server-certs/dev.cac.atat.codes.key -out server-certs/dev.cac.atat.codes.csr -reqexts SAN -config <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend"))
|
||||
|
||||
# Sign the request with your root key
|
||||
openssl x509 -sha256 -req -in server-certs/dev.cac.atat.codes.csr -CA certificate-authority/ca.crt -CAkey certificate-authority/ca.key -CAcreateserial -out server-certs/dev.cac.atat.codes.crt -days 7300 -extfile <(cat req.cnf <(printf "[SAN]\nsubjectAltName=DNS.1:dev.cac.atat.codes,DNS.2:cac.atat.codes,DNS.3:backend")) -extensions SAN
|
||||
|
||||
# Check your homework:
|
||||
openssl verify -CAfile certificate-authority/ca.crt server-certs/dev.cac.atat.codes.crt
|
@ -38,6 +38,7 @@
|
||||
@import "components/dod_login_notice.scss";
|
||||
@import "components/sticky_cta.scss";
|
||||
@import "components/error_page.scss";
|
||||
@import "components/member_form.scss";
|
||||
|
||||
@import "sections/login";
|
||||
@import "sections/home";
|
||||
|
@ -3,8 +3,7 @@
|
||||
background-color: $color-white;
|
||||
border-top: 1px solid $color-gray-lightest;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
padding: $gap * 1.5;
|
||||
position: fixed;
|
||||
@ -15,19 +14,7 @@
|
||||
color: $color-gray-dark;
|
||||
font-size: 1.5rem;
|
||||
|
||||
&__info {
|
||||
flex-grow: 1;
|
||||
&__login {
|
||||
padding-left: 0.8rem;
|
||||
|
||||
&__link {
|
||||
margin: (-$gap * 2) (-$gap);
|
||||
font-weight: normal;
|
||||
|
||||
.icon--footer {
|
||||
@include icon-size(16);
|
||||
|
||||
margin: 0rem 0.8rem 0rem 0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Form Grid
|
||||
.form-row {
|
||||
margin: ($gap * 4) 0;
|
||||
&--separated {
|
||||
|
||||
&--bordered {
|
||||
border-bottom: $color-gray-lighter 1px solid;
|
||||
}
|
||||
|
||||
@ -202,3 +203,7 @@
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container__half {
|
||||
max-width: 46rem;
|
||||
}
|
||||
|
@ -4,18 +4,4 @@
|
||||
height: auto;
|
||||
box-shadow: $box-shadow;
|
||||
margin-bottom: -$footer-height * 2.5;
|
||||
|
||||
.sidenav__link {
|
||||
padding-right: $gap * 2;
|
||||
|
||||
@include media($large-screen) {
|
||||
padding-right: $gap * 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__context--portfolio {
|
||||
.sidenav__link {
|
||||
padding-right: $gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
styles/components/_member_form.scss
Normal file
61
styles/components/_member_form.scss
Normal file
@ -0,0 +1,61 @@
|
||||
.member-form {
|
||||
text-align: left;
|
||||
|
||||
input[type="checkbox"] + label::before {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
text-align: left;
|
||||
|
||||
.usa-input__choices label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
padding: $gap * 2;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
|
||||
&.checked {
|
||||
border: 1px solid $color-blue;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
p.usa-input__help {
|
||||
margin-bottom: 0;
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.usa-input {
|
||||
width: 45rem;
|
||||
|
||||
input,
|
||||
label,
|
||||
.usa-input__message {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
label .icon-validation {
|
||||
left: unset;
|
||||
right: -$gap * 4;
|
||||
}
|
||||
|
||||
&--validation--phoneExt {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#modal--add-app-mem,
|
||||
#modal--add-portfolio-manager {
|
||||
.modal__body {
|
||||
min-width: 75rem;
|
||||
}
|
||||
}
|
@ -5,13 +5,6 @@
|
||||
}
|
||||
|
||||
margin-left: 2 * $gap;
|
||||
|
||||
.line {
|
||||
box-sizing: border-box;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
border: 1px solid $color-gray-lightest;
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-header {
|
||||
@ -40,36 +33,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__budget {
|
||||
font-size: $small-font-size;
|
||||
align-items: center;
|
||||
|
||||
.icon-tooltip {
|
||||
margin-left: -$gap / 2;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&--dollars {
|
||||
font-size: $h2-font-size;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&--amount {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&--cents {
|
||||
font-size: 2rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-left: -0.7rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
justify-content: center;
|
||||
font-size: $small-font-size;
|
||||
@ -109,22 +72,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-left {
|
||||
width: 12.5rem;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
margin-left: -0.4rem;
|
||||
}
|
||||
|
||||
.unfunded {
|
||||
color: $color-red;
|
||||
.icon {
|
||||
@include icon-color($color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin subheading {
|
||||
@ -138,6 +85,10 @@
|
||||
.portfolio-content {
|
||||
margin: (4 * $gap) $gap 0 $gap;
|
||||
|
||||
.panel {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
a.add-new-button {
|
||||
display: inherit;
|
||||
margin-left: auto;
|
||||
@ -157,44 +108,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
input.usa-button.usa-button-primary {
|
||||
width: 9rem;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
select {
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
.members-table-ppoc {
|
||||
select::-ms-expand {
|
||||
display: none;
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
float: right;
|
||||
margin: 5px 0px;
|
||||
padding: 0px 24px;
|
||||
background-image: none;
|
||||
-ms-word-break: normal;
|
||||
word-break: normal;
|
||||
padding-right: 3rem;
|
||||
padding-left: 1.2rem;
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
select:hover {
|
||||
box-shadow: none;
|
||||
color: $color-gray;
|
||||
}
|
||||
}
|
||||
|
||||
a.modal-link.icon-link {
|
||||
float: right;
|
||||
|
||||
@ -607,3 +520,28 @@
|
||||
margin-right: $gap * 3;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
border-right: 1px solid $color-gray-light;
|
||||
margin-right: $gap * 3;
|
||||
padding-right: $gap * 3;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
&__header {
|
||||
margin: 0;
|
||||
&-icon {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
&__value {
|
||||
font-size: $lead-font-size;
|
||||
&--large {
|
||||
font-size: 3.4rem;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,4 +63,8 @@
|
||||
font-size: $small-font-size;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
&--link {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,10 @@
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.topbar__link {
|
||||
color: $color-white;
|
||||
&__link {
|
||||
color: $color-white !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: $topbar-height;
|
||||
@ -38,62 +39,17 @@
|
||||
padding-left: $gap / 2;
|
||||
}
|
||||
|
||||
&--shield {
|
||||
width: $icon-bar-width;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
|
||||
.topbar__link-icon {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-primary-darker;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar__context {
|
||||
&__context {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
|
||||
.topbar__portfolio-menu {
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.topbar--public {
|
||||
background-color: $color-primary;
|
||||
|
||||
.topbar__navigation {
|
||||
justify-content: flex-end;
|
||||
-ms-flex-pack: justify;
|
||||
}
|
||||
|
||||
.topbar__link {
|
||||
color: $color-white;
|
||||
|
||||
&-icon {
|
||||
@include icon-style-inverted;
|
||||
}
|
||||
|
||||
&--home {
|
||||
padding-left: $gap;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-primary-darker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,5 +88,9 @@ p {
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid $color-gray-light;
|
||||
margin: ($gap * 3) 0;
|
||||
|
||||
&.full-width {
|
||||
margin: ($gap * 3) ($site-margins * -4);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ $usa-banner-height: 2.8rem;
|
||||
$sidenav-expanded-width: 25rem;
|
||||
$sidenav-collapsed-width: 10rem;
|
||||
$max-panel-width: 80rem;
|
||||
$home-pg-icon-width: 6rem;
|
||||
|
||||
/*
|
||||
* USWDS Variables
|
||||
|
@ -12,6 +12,7 @@
|
||||
.usa-button,
|
||||
a {
|
||||
margin: 0 0 0 $gap;
|
||||
cursor: pointer;
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin: 0 0 0 ($gap * 2);
|
||||
@ -46,7 +47,7 @@
|
||||
background: white;
|
||||
right: 0;
|
||||
padding-right: $gap * 4;
|
||||
border-top: 1px solid $color-gray-light;
|
||||
border-top: 1px solid $color-gray-lighter;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
@ -94,4 +94,19 @@
|
||||
&--primary {
|
||||
@include icon-color($color-primary);
|
||||
}
|
||||
|
||||
&--home-pg-badge {
|
||||
@include icon-size(27);
|
||||
@include icon-color($color-white);
|
||||
|
||||
background-color: $color-primary;
|
||||
height: $home-pg-icon-width;
|
||||
width: $home-pg-icon-width;
|
||||
border-radius: 100%;
|
||||
display: inline-flex;
|
||||
|
||||
svg {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,6 +165,15 @@
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: 3rem;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: -3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
|
@ -1,10 +1,4 @@
|
||||
@mixin sidenav__header {
|
||||
padding: $gap ($gap * 2);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sidenav-container {
|
||||
box-shadow: $box-shadow;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
top: $topbar-height + $usa-banner-height;
|
||||
@ -14,46 +8,74 @@
|
||||
@extend .sidenav-container;
|
||||
width: $sidenav-collapsed-width;
|
||||
}
|
||||
}
|
||||
|
||||
&__fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.sidenav {
|
||||
.sidenav {
|
||||
width: $sidenav-expanded-width;
|
||||
position: fixed;
|
||||
|
||||
&--minimized {
|
||||
@extend .sidenav;
|
||||
|
||||
width: $sidenav-collapsed-width;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
@include media($large-screen) {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include sidenav__header;
|
||||
&__header {
|
||||
padding: $gap ($gap * 2);
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $color-gray-lighter;
|
||||
|
||||
font-size: $h3-font-size;
|
||||
&--minimized {
|
||||
@extend .sidenav__header;
|
||||
|
||||
padding: $gap;
|
||||
width: $sidenav-collapsed-width;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $h6-font-size;
|
||||
text-transform: uppercase;
|
||||
width: 50%;
|
||||
color: $color-gray-dark;
|
||||
opacity: 0.54;
|
||||
white-space: nowrap;
|
||||
padding: $gap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__toggle {
|
||||
@include sidenav__header;
|
||||
|
||||
font-size: $small-font-size;
|
||||
line-height: 2.8rem;
|
||||
float: right;
|
||||
color: $color-blue-darker;
|
||||
color: $color-blue;
|
||||
text-decoration: none;
|
||||
padding: $gap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle-arrows {
|
||||
&__toggle-arrows {
|
||||
vertical-align: middle;
|
||||
@include icon-size(20);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
&.sidenav__list--padded {
|
||||
margin-top: 4 * $gap;
|
||||
&__list {
|
||||
margin-top: 3 * $gap;
|
||||
margin-bottom: $footer-height;
|
||||
padding-bottom: $gap;
|
||||
padding-bottom: ($gap * 2);
|
||||
position: fixed;
|
||||
overflow-y: scroll;
|
||||
top: $topbar-height + $usa-banner-height + 4rem;
|
||||
@ -61,145 +83,47 @@
|
||||
left: 0;
|
||||
width: $sidenav-expanded-width;
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
display: block;
|
||||
&--no-header {
|
||||
top: $topbar-height + $usa-banner-height;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider--small {
|
||||
display: block;
|
||||
width: 4 * $gap;
|
||||
border: 1px solid #d6d7d9;
|
||||
margin-left: 2 * $gap;
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin: 2 * $gap;
|
||||
color: $color-gray;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&__item {
|
||||
margin: 0;
|
||||
display: block;
|
||||
color: $color-black-light !important;
|
||||
}
|
||||
|
||||
&__link {
|
||||
display: block;
|
||||
padding: $gap ($gap * 2);
|
||||
color: $color-black;
|
||||
text-decoration: underline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: $color-black-light !important;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&-icon {
|
||||
margin-left: -($gap * 0.5);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: $color-shadow;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--add {
|
||||
color: $color-blue;
|
||||
font-size: $small-font-size;
|
||||
|
||||
.icon {
|
||||
@include icon-color($color-blue);
|
||||
@include icon-size(14);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
@include h4;
|
||||
|
||||
color: $color-primary;
|
||||
background-color: $color-aqua-lightest;
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-style-active;
|
||||
}
|
||||
|
||||
background-color: $color-aqua-lightest !important;
|
||||
color: $color-primary-darker !important;
|
||||
box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;
|
||||
position: relative;
|
||||
|
||||
&_indicator .icon {
|
||||
@include icon-color($color-primary);
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
+ ul {
|
||||
background-color: $color-primary;
|
||||
|
||||
.sidenav__link {
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-blue-darker;
|
||||
}
|
||||
|
||||
&--active {
|
||||
@include h5;
|
||||
|
||||
color: $color-white;
|
||||
background-color: $color-primary;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include icon-color($color-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ ul {
|
||||
li {
|
||||
.sidenav__link {
|
||||
@include h5;
|
||||
|
||||
padding: $gap * 0.75;
|
||||
padding-left: 4.5rem;
|
||||
border: 0;
|
||||
font-weight: normal;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-size(12);
|
||||
|
||||
flex-shrink: 0;
|
||||
margin-right: 1.5rem;
|
||||
margin-left: -3rem;
|
||||
}
|
||||
|
||||
.sidenav__link-label {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
color: $color-primary !important;
|
||||
background-color: $color-aqua-lightest;
|
||||
|
||||
.sidenav__link-icon {
|
||||
@include icon-style-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--minimized {
|
||||
@extend .sidenav;
|
||||
|
||||
width: $sidenav-collapsed-width;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
@ -21,24 +21,8 @@ table.atat-table {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&--align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--shrink {
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
&--expand {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--hide-small {
|
||||
display: none;
|
||||
|
||||
@include media($medium-screen) {
|
||||
display: table-cell;
|
||||
}
|
||||
&--third {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,7 +37,6 @@ table.atat-table {
|
||||
padding: $gap * 2;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
display: table-cell;
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
|
||||
&:first-child {
|
||||
@ -84,28 +67,6 @@ table.atat-table {
|
||||
|
||||
@include panel-margin;
|
||||
|
||||
&__header {
|
||||
@include panel-base;
|
||||
@include panel-theme-default;
|
||||
|
||||
border-top: none;
|
||||
border-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $gap * 2;
|
||||
|
||||
&__title {
|
||||
@include h4;
|
||||
|
||||
font-size: $lead-font-size;
|
||||
justify-content: space-between;
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -23,66 +23,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#modal--add-app-mem,
|
||||
.form-content--app-mem {
|
||||
text-align: left;
|
||||
|
||||
.modal__body {
|
||||
min-width: 75rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + label::before {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
text-align: left;
|
||||
|
||||
.usa-input__choices label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.input__inline-fields {
|
||||
padding: $gap * 2;
|
||||
border: 1px solid $color-gray-lighter;
|
||||
|
||||
&.checked {
|
||||
border: 1px solid $color-blue;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
p.usa-input__help {
|
||||
margin-bottom: 0;
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.application-member__user-info {
|
||||
.usa-input {
|
||||
width: 45rem;
|
||||
|
||||
input,
|
||||
label,
|
||||
.usa-input__message {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
label .icon-validation {
|
||||
left: unset;
|
||||
right: -$gap * 4;
|
||||
}
|
||||
|
||||
&--validation--phoneExt {
|
||||
width: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.environment-roles {
|
||||
padding: 0 ($gap * 3) ($gap * 3);
|
||||
|
||||
|
@ -1,48 +1,24 @@
|
||||
.home {
|
||||
margin: $gap * 3;
|
||||
.sticky-cta {
|
||||
margin: -1.6rem -1.6rem 0 -1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.about-cloud {
|
||||
margin: 4rem auto;
|
||||
&__content {
|
||||
margin: 4rem;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.your-project {
|
||||
margin-top: 3rem;
|
||||
padding: 3rem;
|
||||
background-color: $color-gray-lightest;
|
||||
&--descriptions {
|
||||
.col {
|
||||
margin-left: $home-pg-icon-width;
|
||||
padding: ($gap * 2) ($gap * 4);
|
||||
position: relative;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
.icon--home-pg-badge {
|
||||
position: absolute;
|
||||
left: -$home-pg-icon-width;
|
||||
top: $gap * 3;
|
||||
}
|
||||
|
||||
.links {
|
||||
justify-content: flex-start;
|
||||
|
||||
.icon-link {
|
||||
padding: $gap ($gap * 4);
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
color: $color-gray-dark;
|
||||
|
||||
.svg * {
|
||||
fill: $color-gray-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.active:hover {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,8 +88,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#jedi-heirarchy {
|
||||
max-width: 65rem;
|
||||
margin-top: $gap * 8;
|
||||
}
|
||||
|
@ -118,27 +118,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.reporting-summary-item {
|
||||
border-right: 1px solid $color-gray-light;
|
||||
margin-right: $gap * 3;
|
||||
padding-right: $gap * 3;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
&__header {
|
||||
margin: 0;
|
||||
&-icon {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
&__value {
|
||||
font-size: $lead-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.reporting-expended-funding {
|
||||
&__header {
|
||||
margin: 0;
|
||||
|
@ -20,10 +20,7 @@
|
||||
}
|
||||
|
||||
&__header {
|
||||
.h2,
|
||||
p {
|
||||
margin-bottom: $gap * 0.5;
|
||||
}
|
||||
margin-bottom: $gap * 6;
|
||||
}
|
||||
|
||||
.col {
|
||||
@ -39,22 +36,6 @@
|
||||
margin-top: $gap * 2;
|
||||
}
|
||||
|
||||
.task-order__review {
|
||||
.h2 {
|
||||
margin-bottom: $gap * 4;
|
||||
}
|
||||
|
||||
.task-order__number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.totals-box {
|
||||
flex-grow: unset;
|
||||
display: table;
|
||||
min-width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: ($gap * 4) ($gap * 5) 0;
|
||||
|
||||
@ -155,6 +136,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__confirmation {
|
||||
margin-left: $gap * 8;
|
||||
}
|
||||
}
|
||||
|
||||
.task-order__modal-cancel {
|
||||
@ -170,3 +155,16 @@
|
||||
padding-bottom: $gap * 2.5;
|
||||
}
|
||||
}
|
||||
|
||||
table.clin-summary {
|
||||
tbody,
|
||||
thead {
|
||||
tr {
|
||||
&:last-child {
|
||||
td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@
|
||||
{{ CheckboxInput(form.perms_env_mgmt, classes="input__inline-fields", key=env_mgmt, id=env_mgmt, optional=True) }}
|
||||
{{ CheckboxInput(form.perms_del_env, classes="input__inline-fields", key=del_env, id=del_env, optional=True) }}
|
||||
</div>
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
<div class="environment_roles environment-roles-new">
|
||||
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
|
||||
<p class='usa-input__help subtitle'>
|
||||
@ -118,7 +118,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro InfoFields(member_form) %}
|
||||
<div class="application-member__user-info">
|
||||
<div class="user-info">
|
||||
{{ TextInput(member_form.first_name, validation='requiredField', optional=False) }}
|
||||
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
|
||||
{{ TextInput(member_form.email, validation='email', optional=False) }}
|
||||
|
@ -1,12 +1,10 @@
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/label.html" import Label %}
|
||||
{% import "applications/fragments/new_member_modal_content.html" as member_steps %}
|
||||
{% import "components/member_form.html" as member_form %}
|
||||
{% import "applications/fragments/member_form_fields.html" as member_fields %}
|
||||
{% from "components/modal.html" import Modal %}
|
||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||
{% from "components/save_button.html" import SaveButton %}
|
||||
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
|
||||
|
||||
{% macro MemberManagementTemplate(
|
||||
application,
|
||||
@ -40,7 +38,7 @@
|
||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
</div>
|
||||
<base-form inline-template>
|
||||
<form id='{{ modal_name }}' method="POST" action="{{ url_for(action_update, application_id=application.id, application_role_id=member.role_id,) }}">
|
||||
@ -59,7 +57,7 @@
|
||||
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
||||
<div class="modal__form--header">
|
||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||
<hr>
|
||||
<hr class="full-width">
|
||||
</div>
|
||||
<base-form inline-template :enable-save="true">
|
||||
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('applications.resend_invite', application_id=application.id, application_role_id=member.role_id) }}">
|
||||
@ -179,8 +177,19 @@
|
||||
form=new_member_form,
|
||||
form_action=url_for(action_new, application_id=application.id),
|
||||
steps=[
|
||||
member_steps.MemberStepOne(new_member_form),
|
||||
member_steps.MemberStepTwo(new_member_form, application)
|
||||
member_form.BasicStep(
|
||||
title="portfolios.applications.members.form.add_member"|translate,
|
||||
form=member_fields.InfoFields(new_member_form.user_data),
|
||||
next_button_text="portfolios.applications.members.form.next_button"|translate,
|
||||
previous=False,
|
||||
modal=new_member_modal_name,
|
||||
),
|
||||
member_form.SubmitStep(
|
||||
name=new_member_modal_name,
|
||||
form=member_fields.PermsFields(form=new_member_form, new=True),
|
||||
submit_text="portfolios.applications.members.form.add_member"|translate,
|
||||
modal=new_member_modal_name,
|
||||
)
|
||||
],
|
||||
) }}
|
||||
{% endif %}
|
||||
|
@ -1,50 +0,0 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% import "applications/fragments/member_form_fields.html" as member_fields %}
|
||||
|
||||
{% macro MemberFormTemplate(title=None, next_button=None, previous=True) %}
|
||||
<hr>
|
||||
{% if title %} <h1>{{ title }}</h1> {% endif %}
|
||||
|
||||
{{ caller() }}
|
||||
|
||||
<div class='action-group'>
|
||||
{{ next_button }}
|
||||
{% if previous %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="previous()"
|
||||
class='action-group__action usa-button usa-button-secondary'
|
||||
value='{{ "common.previous" | translate }}'>
|
||||
{% endif %}
|
||||
<a class='action-group__action' v-on:click="closeModal('{{ new_port_mem }}')">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro MemberStepOne(member_form) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="next()"
|
||||
v-bind:disabled="!canSave"
|
||||
class='action-group__action usa-button'
|
||||
value='{{ "portfolios.applications.members.form.next_button" | translate }}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberFormTemplate(title="portfolios.applications.members.form.add_member"|translate, next_button=next_button, previous=False) %}
|
||||
{{ member_fields.InfoFields(member_form.user_data) }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
{% macro MemberStepTwo(member_form, application) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type="submit"
|
||||
class='action-group__action usa-button'
|
||||
form="add-app-mem"
|
||||
v-bind:disabled="!canSave"
|
||||
value='{{ "portfolios.applications.members.form.add_member" | translate}}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberFormTemplate(next_button=next_button) %}
|
||||
{{ member_fields.PermsFields(form=member_form, new=True) }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
@ -30,7 +30,7 @@
|
||||
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="panel__break">
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
<div class="form-col form-col--two-thirds">
|
||||
{{ TextInput(form.description, paragraph=True, optional=True) }}
|
||||
|
@ -19,7 +19,7 @@
|
||||
<p>
|
||||
{{ 'portfolios.applications.new.step_2_description' | translate }}
|
||||
</p>
|
||||
<hr class="panel__break">
|
||||
<hr>
|
||||
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit">
|
||||
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<p>
|
||||
{{ ('portfolios.applications.new.step_3_description' | translate) }}
|
||||
</p>
|
||||
<hr class="panel__break">
|
||||
<hr>
|
||||
|
||||
{{ MemberManagementTemplate(
|
||||
application,
|
||||
|
@ -13,6 +13,9 @@
|
||||
|
||||
{% block application_content %}
|
||||
|
||||
{% if show_flash -%}
|
||||
{% include "fragments/flash.html" %}
|
||||
{%- endif %}
|
||||
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
|
||||
|
||||
{% if user_can(permissions.EDIT_APPLICATION) %}
|
||||
@ -59,59 +62,8 @@
|
||||
environments_obj,
|
||||
new_env_form) }}
|
||||
|
||||
{% if user_can(permissions.DELETE_APPLICATION) %}
|
||||
{% set env_count = application.environments | length %}
|
||||
{% if env_count == 1 %}
|
||||
{% set pluralized_env = "environment" %}
|
||||
{% else %}
|
||||
{% set pluralized_env = "environments" %}
|
||||
{% endif %}
|
||||
|
||||
<h3>
|
||||
{{ "portfolios.applications.delete.subheading" | translate }}
|
||||
</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-col form-col--two-thirds">
|
||||
{{ "portfolios.applications.delete.text" | translate({"application_name": application.name}) | safe }}
|
||||
</div>
|
||||
<div class="form-col form-col--third">
|
||||
<div class="usa-input">
|
||||
<input
|
||||
id="delete-application"
|
||||
type="button"
|
||||
v-on:click="openModal('delete-application')"
|
||||
class='usa-button--outline button-danger-outline'
|
||||
value="{{ 'portfolios.applications.delete.button' | translate }}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% call Modal(name="delete-application") %}
|
||||
<h1>{{ "portfolios.applications.delete.header" | translate }}</h1>
|
||||
<hr>
|
||||
{{
|
||||
Alert(
|
||||
title=("components.modal.destructive_title" | translate),
|
||||
message=("portfolios.applications.delete.alert.message" | translate),
|
||||
level="warning"
|
||||
)
|
||||
}}
|
||||
|
||||
{{
|
||||
DeleteConfirmation(
|
||||
modal_id="delete_application",
|
||||
delete_text=('portfolios.applications.delete.button' | translate),
|
||||
delete_action= url_for('applications.delete', application_id=application.id),
|
||||
form=application_form
|
||||
)
|
||||
}}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
{% if user_can(permissions.VIEW_APPLICATION_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %}
|
||||
<hr>
|
||||
{% include "fragments/audit_events_log.html" %}
|
||||
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
|
||||
{% endif %}
|
||||
|
@ -21,9 +21,7 @@
|
||||
{% include 'navigation/topbar.html' %}
|
||||
|
||||
<div class='global-layout'>
|
||||
{% if portfolios %}
|
||||
{% include 'navigation/global_sidenav.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div class='global-panel-container'>
|
||||
{% block sidenav %}{% endblock %}
|
||||
|
@ -9,11 +9,15 @@
|
||||
"text": "changes pending",
|
||||
"color": "default",
|
||||
},
|
||||
"ppoc": {"text": "primary point of contact"}
|
||||
} %}
|
||||
|
||||
{% if type -%}
|
||||
{% if type in label_info.keys() -%}
|
||||
<span class='label label--{{ label_info[type]["color"] }} {{ classes }}'>
|
||||
{{ Icon(label_info[type]["icon"]) }} {{ label_info[type]["text"] }}
|
||||
{% if label_info[type]["icon"] %}
|
||||
{{ Icon(label_info[type]["icon"]) }}
|
||||
{% endif %}
|
||||
{{ label_info[type]["text"] }}
|
||||
</span>
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
65
templates/components/member_form.html
Normal file
65
templates/components/member_form.html
Normal file
@ -0,0 +1,65 @@
|
||||
<!-- Layout macro -->
|
||||
{% macro MemberForm(title=None, next_button=None, previous=True, modal=modal) %}
|
||||
<div class="member-form">
|
||||
<hr class="full-width">
|
||||
{% if title %} <h2>{{ title }}</h2> {% endif %}
|
||||
|
||||
{{ caller() }}
|
||||
</div>
|
||||
<div class='action-group'>
|
||||
{{ next_button }}
|
||||
{% if previous %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="previous()"
|
||||
class='action-group__action usa-button usa-button-secondary'
|
||||
value='{{ "common.previous" | translate }}'>
|
||||
{% endif %}
|
||||
<a class='action-group__action' v-on:click="closeModal('{{ modal }}')">{{ "common.cancel" | translate }}</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<!-- Step macros to use with MultiStepModalForm -->
|
||||
{% macro BasicStep(
|
||||
title=None,
|
||||
form=form,
|
||||
next_button_text=next_button_text,
|
||||
previous=True,
|
||||
modal=modal
|
||||
) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type='button'
|
||||
v-on:click="next()"
|
||||
v-bind:disabled="!canSave"
|
||||
class='action-group__action usa-button'
|
||||
value='{{ next_button_text }}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
|
||||
{{ form }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro SubmitStep(
|
||||
name=name,
|
||||
title=None,
|
||||
form=form,
|
||||
submit_text=submit_text,
|
||||
previous=True,
|
||||
modal=modal
|
||||
) %}
|
||||
{% set next_button %}
|
||||
<input
|
||||
type="submit"
|
||||
class='action-group__action usa-button'
|
||||
form="{{ name }}"
|
||||
v-bind:disabled="!canSave"
|
||||
value='{{ submit_text }}'>
|
||||
{% endset %}
|
||||
|
||||
{% call MemberForm(title=title, next_button=next_button, previous=previous, modal=modal) %}
|
||||
{{ form }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
@ -1,35 +1,9 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
|
||||
<li>
|
||||
{% macro SidenavItem(label, href, active=False) -%}
|
||||
<li class="sidenav__item">
|
||||
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
|
||||
{% if icon %}
|
||||
{{ Icon(icon, classes="sidenav__link-icon") }}
|
||||
{% endif %}
|
||||
|
||||
<span class="sidenav__link-label">
|
||||
{{label}}
|
||||
</span>
|
||||
{% if active %}
|
||||
<span class="sidenav__link-active_indicator">
|
||||
{{ Icon("caret_right") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
{% if subnav and active %}
|
||||
<ul>
|
||||
{% for item in subnav %}
|
||||
<li>
|
||||
<a class="sidenav__link {% if item["active"] %}sidenav__link--active{% endif %}" href="{{item["href"]}}" title="{{item["label"]}}">
|
||||
{% if "icon" in item %}
|
||||
{{ Icon(item["icon"], classes="sidenav__link-icon") }}
|
||||
{% endif %}
|
||||
<span class="sidenav__link-label">{{item["label"]}}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{%- endmacro %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% macro Tooltip(message,title='Help', classes="") %}
|
||||
|
||||
<button type="button" tabindex="0" class="icon-tooltip {{classes}}" v-tooltip.top="{content: '{{message}}', container: false}">
|
||||
{{ Icon('help') }}<span>{{ title }}</span>
|
||||
{{ Icon('question') }}<span>{{ title }}</span>
|
||||
</button>
|
||||
|
||||
{%- endmacro %}
|
||||
|
@ -1,14 +1,8 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
<footer class='app-footer'>
|
||||
<div class='app-footer__info'>
|
||||
<a href="#" class='icon-link app-footer__info__link' target='_blank' rel='noopener noreferrer'>
|
||||
{{ Icon('help', classes='icon--footer') }}
|
||||
<span>{{ "footer.jedi_help_link_text" | translate }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if g.last_login %}
|
||||
<div class="">
|
||||
<div class="app-footer__login">
|
||||
{{ "footer.login" | translate }} <local-datetime timestamp='{{ g.last_login }}' format='MMM D YYYY H:mm Z'></local-datetime>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -6,31 +6,26 @@
|
||||
|
||||
{% block content %}
|
||||
<div class='global-layout'>
|
||||
<div class='global-navigation sidenav'>
|
||||
<ul>
|
||||
|
||||
<div class='global-navigation'>
|
||||
<div class="sidenav-container">
|
||||
<div class="sidenav">
|
||||
<ul class="sidenav__list sidenav__list--no-header">
|
||||
{{ SidenavItem("JEDI Cloud Help",
|
||||
href = url_for("atst.helpdocs"),
|
||||
active = not doc,
|
||||
icon='help'
|
||||
)}}
|
||||
|
||||
{% for doc_item in docs %}
|
||||
{% set active = doc and doc == doc_item %}
|
||||
|
||||
{{ SidenavItem(doc_item | title,
|
||||
href = url_for("atst.helpdocs", doc=doc_item),
|
||||
active = active,
|
||||
subnav = subnav or None
|
||||
)}}
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class='global-panel-container'>
|
||||
|
||||
<div class='panel'>
|
||||
<div class='panel__heading panel__heading--divider'>
|
||||
<h1>
|
||||
@ -42,16 +37,12 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class='panel__content'>
|
||||
|
||||
{% block doc_content %}
|
||||
<p>Welcome to the JEDI Cloud help documentation.</p>
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,8 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %}
|
||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@ -13,88 +12,56 @@
|
||||
{% set sticky_header = "home.get_started" | translate %}
|
||||
{% endif %}
|
||||
|
||||
{% call StickyCTA(sticky_header) %}
|
||||
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button-primary">
|
||||
{{ "home.add_portfolio_button_text" | translate }}
|
||||
</a>
|
||||
{% endcall %}
|
||||
|
||||
<div class="about-cloud">
|
||||
<div class="home__content">
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<h1>{{ "home.head" | translate }}</h1>
|
||||
|
||||
{{ SemiCollapsibleText(first_half=("home.about_cloud.part1"|translate), second_half=("home.about_cloud.part2"|translate)) }}
|
||||
|
||||
<div class="your-project">
|
||||
<h2 class="h3">{{ "home.your_project" | translate }}</h2>
|
||||
<p>{{ "home.your_project_descrip" | translate }}</p>
|
||||
|
||||
<h3>Set up a Portfolio</h3>
|
||||
<h4>New Portfolios will be visible in the left side bar of this page. </h4>
|
||||
<p>All TOs associated to a specific Application or set of related Applications will be entered at the Portfolio level. Funding is applied and managed at the Portfolio level as well.</p>
|
||||
<hr>
|
||||
|
||||
{% macro Link(icon, text, section, default=False) %}
|
||||
{% if default %}
|
||||
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}" || selectedSection === null}' v-on:click="toggleSection('{{ section }}')">
|
||||
{% else %}
|
||||
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}"}' v-on:click="toggleSection('{{ section }}')">
|
||||
{% endif %}
|
||||
<div class="col">
|
||||
<div class='icon-link--icon'>{{ Icon(icon) }}</div>
|
||||
<div class='icon-link--name'>{{ text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<toggler inline-template v-bind:initial-selected-section="'funding'">
|
||||
<div>
|
||||
<div class="portfolio-header">
|
||||
<div class="links row">
|
||||
{{ Link(
|
||||
icon='funding',
|
||||
section='funding',
|
||||
text='navigation.portfolio_navigation.breadcrumbs.funding' | translate,
|
||||
default=True
|
||||
) }}
|
||||
{{ Link(
|
||||
icon='applications',
|
||||
section='applications',
|
||||
text='navigation.portfolio_navigation.breadcrumbs.applications' | translate,
|
||||
) }}
|
||||
{{ Link(
|
||||
icon='chart-pie',
|
||||
section='reports',
|
||||
text='navigation.portfolio_navigation.breadcrumbs.reports' | translate,
|
||||
) }}
|
||||
{{ Link(
|
||||
icon='cog',
|
||||
section='admin',
|
||||
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
{% macro Description(section, default=False) %}
|
||||
{% if default %}
|
||||
<p v-show="selectedSection === '{{ section }}' || selectedSection === null">
|
||||
{% else %}
|
||||
<p v-show="selectedSection === '{{ section }}'">
|
||||
{% endif %}
|
||||
<strong>
|
||||
{{ "navigation.portfolio_navigation.breadcrumbs.%s" | format(section) | translate }}
|
||||
</strong>
|
||||
{{ "home.%s_descrip" | format(section) | translate }}
|
||||
<div class="home__content--descriptions">
|
||||
<div class="row">
|
||||
<div class="col col--half col--pad">
|
||||
{{ Icon('funding', classes="icon--home-pg-badge") }}
|
||||
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.funding" | translate }}</h4>
|
||||
<p>
|
||||
{{ "home.funding_descrip" | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col col--half col--pad">
|
||||
{{ Icon('chart-pie', classes="icon--home-pg-badge") }}
|
||||
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.reports" | translate }}</h4>
|
||||
<p>
|
||||
{{ "home.reports_descrip" | translate }}
|
||||
</p>
|
||||
{% endmacro %}
|
||||
<div class="project-section-descriptions">
|
||||
{{ Description('funding', default=True) }}
|
||||
{{ Description('applications') }}
|
||||
{{ Description('reports') }}
|
||||
{{ Description('admin') }}
|
||||
</div>
|
||||
</div>
|
||||
</toggler>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col--half col--pad">
|
||||
{{ Icon('applications', classes="icon--home-pg-badge") }}
|
||||
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.applications" | translate }}</h4>
|
||||
<p>
|
||||
{{ "home.applications_descrip" | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col col--half col--pad">
|
||||
{{ Icon('cog', classes="icon--home-pg-badge") }}
|
||||
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.admin" | translate }}</h4>
|
||||
<p>
|
||||
{{ "home.admin_descrip" | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__footer">
|
||||
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button usa-button-primary">
|
||||
{{ "home.add_portfolio_button_text" | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<img id='jedi-heirarchy' src="{{ url_for("static", filename="img/JEDIhierarchyDiagram.png")}}" alt="JEDI heirarchy diagram">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user