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$",
|
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
|
||||||
"lines": null
|
"lines": null
|
||||||
},
|
},
|
||||||
"generated_at": "2019-12-06T21:22:07Z",
|
"generated_at": "2020-01-09T11:35:03Z",
|
||||||
"plugins_used": [
|
"plugins_used": [
|
||||||
{
|
{
|
||||||
"base64_limit": 4.5,
|
"base64_limit": 4.5,
|
||||||
@ -98,7 +98,7 @@
|
|||||||
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
|
||||||
"is_secret": false,
|
"is_secret": false,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 29,
|
"line_number": 30,
|
||||||
"type": "Secret Keyword"
|
"type": "Secret Keyword"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -111,15 +111,6 @@
|
|||||||
"type": "Secret Keyword"
|
"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": [
|
"ssl/client-certs/atat.mil.key": [
|
||||||
{
|
{
|
||||||
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
|
"hashed_secret": "be4fc4886bd949b369d5e092eb87494f12e57e5b",
|
||||||
@ -170,7 +161,7 @@
|
|||||||
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
|
||||||
"is_secret": false,
|
"is_secret": false,
|
||||||
"is_verified": false,
|
"is_verified": false,
|
||||||
"line_number": 656,
|
"line_number": 665,
|
||||||
"type": "Hex High Entropy String"
|
"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/config/ ./config/
|
||||||
COPY --from=builder /install/templates/ ./templates/
|
COPY --from=builder /install/templates/ ./templates/
|
||||||
COPY --from=builder /install/translations.yaml .
|
COPY --from=builder /install/translations.yaml .
|
||||||
COPY --from=builder /install/script/seed_roles.py ./script/seed_roles.py
|
COPY --from=builder /install/script/ ./script/
|
||||||
COPY --from=builder /install/script/sync-crls ./script/sync-crls
|
|
||||||
COPY --from=builder /install/static/ ./static/
|
COPY --from=builder /install/static/ ./static/
|
||||||
COPY --from=builder /install/fixtures/ ./fixtures
|
COPY --from=builder /install/fixtures/ ./fixtures
|
||||||
COPY --from=builder /install/uwsgi.ini .
|
COPY --from=builder /install/uwsgi.ini .
|
||||||
|
2
Pipfile
2
Pipfile
@ -18,7 +18,6 @@ flask-session = "*"
|
|||||||
flask-wtf = "*"
|
flask-wtf = "*"
|
||||||
pyopenssl = "*"
|
pyopenssl = "*"
|
||||||
requests = "*"
|
requests = "*"
|
||||||
apache-libcloud = "*"
|
|
||||||
lockfile = "*"
|
lockfile = "*"
|
||||||
werkzeug = "*"
|
werkzeug = "*"
|
||||||
PyYAML = "*"
|
PyYAML = "*"
|
||||||
@ -30,6 +29,7 @@ azure-graphrbac = "*"
|
|||||||
msrestazure = "*"
|
msrestazure = "*"
|
||||||
azure-mgmt-authorization = "*"
|
azure-mgmt-authorization = "*"
|
||||||
azure-mgmt-managementgroups = "*"
|
azure-mgmt-managementgroups = "*"
|
||||||
|
azure-mgmt-resource = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
|
225
Pipfile.lock
generated
225
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5"
|
"sha256": "c203c47b00f413fd40056ef6d2d8e51b37ad3ff5f7693db5eb170b7f8fd43234"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -25,10 +25,10 @@
|
|||||||
},
|
},
|
||||||
"alembic": {
|
"alembic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:49277bb7242192bbb9eac58fed4fe02ec6c3a2a4b4345d2171197459266482b2"
|
"sha256:3b0cb1948833e062f4048992fbc97ecfaaaac24aaa0d83a1202a99fb58af8c6d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.1"
|
"version": "==1.3.2"
|
||||||
},
|
},
|
||||||
"amqp": {
|
"amqp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -37,14 +37,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.5.2"
|
"version": "==2.5.2"
|
||||||
},
|
},
|
||||||
"apache-libcloud": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f",
|
|
||||||
"sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.6.1"
|
|
||||||
},
|
|
||||||
"azure-common": {
|
"azure-common": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:53b1195b8f20943ccc0e71a17849258f7781bc6db1c72edc7d6c055f79bd54e3",
|
"sha256:53b1195b8f20943ccc0e71a17849258f7781bc6db1c72edc7d6c055f79bd54e3",
|
||||||
@ -76,6 +68,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.2.0"
|
"version": "==0.2.0"
|
||||||
},
|
},
|
||||||
|
"azure-mgmt-resource": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:20b3394e4dc76fbd9459723cb8c0300fb18a8c32100076f023b5470426b9f104",
|
||||||
|
"sha256:eaea8b5d05495d1b74220052275d46b6bed93b59245bcaa747279a52e41c3bdf"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==7.0.0"
|
||||||
|
},
|
||||||
"azure-mgmt-subscription": {
|
"azure-mgmt-subscription": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
|
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
|
||||||
@ -117,11 +117,11 @@
|
|||||||
},
|
},
|
||||||
"celery": {
|
"celery": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:65f4d67fc1037edacecbf39fcf956da68b984cf2a6d89bd73a8a5a80e35e3dd7",
|
"sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f",
|
||||||
"sha256:8a59d80235b876881d9893751f2a87936165fc1347efee66095620b3cadf533b"
|
"sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.4.0rc4"
|
"version": "==4.4.0"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -256,10 +256,11 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
|
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||||
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
|
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||||
],
|
],
|
||||||
"version": "==1.1.0"
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
"isodate": {
|
"isodate": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -284,10 +285,10 @@
|
|||||||
},
|
},
|
||||||
"kombu": {
|
"kombu": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1760b54b1d15a547c9a26d3598a1c8cdaf2436386ac1f5561934bc8a3cbbbd86",
|
"sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac",
|
||||||
"sha256:e7465aa85a1db889116819f08c5de29520d2fa103324dcdca5e90af345f01771"
|
"sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"
|
||||||
],
|
],
|
||||||
"version": "==4.6.6"
|
"version": "==4.6.7"
|
||||||
},
|
},
|
||||||
"lockfile": {
|
"lockfile": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -338,10 +339,10 @@
|
|||||||
},
|
},
|
||||||
"more-itertools": {
|
"more-itertools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
|
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||||
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
|
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||||
],
|
],
|
||||||
"version": "==8.0.0"
|
"version": "==8.0.2"
|
||||||
},
|
},
|
||||||
"msrest": {
|
"msrest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -514,10 +515,10 @@
|
|||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"
|
"sha256:bfb8f464a5000b567ac1d350b9090cf081180ec1ab4aa87e7bca12dab25320ec"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.11"
|
"version": "==1.3.12"
|
||||||
},
|
},
|
||||||
"unipath": {
|
"unipath": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -677,44 +678,46 @@
|
|||||||
},
|
},
|
||||||
"colorama": {
|
"colorama": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
|
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
|
||||||
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
|
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
|
||||||
],
|
],
|
||||||
"version": "==0.4.1"
|
"version": "==0.4.3"
|
||||||
},
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2358e685d0253125da42a48396038d4c7b4cd1448c00bbc4bda0cb8c43c2a870",
|
"sha256:0cd13a6e98c37b510a2d34c8281d5e1a226aaf9b65b7d770ef03c63169965351",
|
||||||
"sha256:25017cf384eeed2e6caf72efd3ec4124e32a8b7a4387600499104387975400c7",
|
"sha256:1a4b6b6a2a3a6612e6361130c2cc3dc4378d8c221752b96167ccbad94b47f3cd",
|
||||||
"sha256:2e2de9423ff8b14303a97eafddd16c479fbcc9a0b8b0be3b7c3843a3e0cf6d69",
|
"sha256:2ee55e6dba516ddf6f484aa83ccabbb0adf45a18892204c23486938d12258cde",
|
||||||
"sha256:324ed908e4e40a6e2451056fe502470ad4e79495cb7a03ecab94e6309c3e117e",
|
"sha256:3be5338a2eb4ef03c57f20917e1d12a1fd10e3853fed060b6d6b677cb3745898",
|
||||||
"sha256:34f865a0cf6255b694a46e4383a7131c61ea72c5b4c4f81d20e522fb1e440b4b",
|
"sha256:44b783b02db03c4777d8cf71bae19eadc171a6f2a96777d916b2c30a1eb3d070",
|
||||||
"sha256:3a2bcc464b60a18f1f7167b95b2773ede93bf3722bfa59e0802717f652b6cc25",
|
"sha256:475bf7c4252af0a56e1abba9606f1e54127cdf122063095c75ab04f6f99cf45e",
|
||||||
"sha256:48d70865266d649b6602e2ba94820d7972ef470d3b72a8fd41a3d17321feed3a",
|
"sha256:47c81ee687eafc2f1db7f03fbe99aab81330565ebc62fb3b61edfc2216a550c8",
|
||||||
"sha256:50cf23523ab3a724c6905d3b60f87fa8250d9bae3995e09f49f63effa2b54f15",
|
"sha256:4a7f8e72b18f2aca288ff02255ce32cc830bc04d993efbc87abf6beddc9e56c0",
|
||||||
"sha256:54c84a68abd8c4c5b71878b35eb85321df41f3d144c78181867d5b026ec74994",
|
"sha256:50197163a22fd17f79086e087a787883b3ec9280a509807daf158dfc2a7ded02",
|
||||||
"sha256:5b59d661ee7f3200aedd7b71882b7927ea7ed522df75e3853f316a79ad872a2e",
|
"sha256:56b13000acf891f700f5067512b804d1ec8c301d627486c678b903859d07f798",
|
||||||
"sha256:5ffb39624bc573177888a21fb301ccee46838c600b27d58c3e9dae495f44d34a",
|
"sha256:79388ae29c896299b3567965dbcd93255f175c17c6c7bca38614d12718c47466",
|
||||||
"sha256:699b3072b7f0e69ed175a88fa8b2ec7eefc4f34d490c54ed9a52feff21a15fdc",
|
"sha256:79fd5d3d62238c4f583b75d48d53cdae759fe04d4fb18fe8b371d88ad2b6f8be",
|
||||||
"sha256:79ef4a2bb862110bd585174e551a783bee5c3aa461734a2ac7429193be357589",
|
"sha256:7fe3e2fde2bf1d7ce25ebcd2d3de3650b8d60d9a73ce6dcef36e20191291613d",
|
||||||
"sha256:8210a6f93c4a8c6d460b402e20e38399529b99200c3318542faf6a520c9b6a5c",
|
"sha256:81042a24f67b96e4287774014fa27220d8a4d91af1043389e4d73892efc89ac6",
|
||||||
"sha256:8d30c10cfd0a6fdf0a2d5023de00ef7b329cd6ead2310c9e53eab79c209acb70",
|
"sha256:81326f1095c53111f8afc95da281e1414185f4a538609a77ca50bdfa39a6c207",
|
||||||
"sha256:97ac79ff28f2cda6ac00a803ee582b965951755f61ab43377482bfba450b619a",
|
"sha256:8873dc0d8f42142ea9f20c27bbdc485190fff93823c6795be661703369e5877d",
|
||||||
"sha256:9fe4aacacff9028ed167db108bf013510654f148d83c4857fed61d2ce0588bf2",
|
"sha256:88d2cbcb0a112f47eef71eb95460b6995da18e6f8ca50c264585abc2c473154b",
|
||||||
"sha256:a5b6395d5957d638f8b1870561607e3c39b1a236ea6cff9eafe5b9bb1db913f2",
|
"sha256:91f2491aeab9599956c45a77c5666d323efdec790bfe23fcceafcd91105d585a",
|
||||||
"sha256:ab32c5fad6905986a7e34e3acf01180a69bb60c2aa7331815b46e51c776a1943",
|
"sha256:979daa8655ae5a51e8e7a24e7d34e250ae8309fd9719490df92cbb2fe2b0422b",
|
||||||
"sha256:ad67f0cfdfecbd49b9da46a7e488e6dc32a69388740b85c36a4ef4b33082cbad",
|
"sha256:9c871b006c878a890c6e44a5b2f3c6291335324b298c904dc0402ee92ee1f0be",
|
||||||
"sha256:aedad67c30326a1af324f45833a40b97180664912deb29942459ddbe9fa0ce19",
|
"sha256:a6d092545e5af53e960465f652e00efbf5357adad177b2630d63978d85e46a72",
|
||||||
"sha256:b077cd0e70f41366ac1f9d09275258fa1906758a5d4f31cacc18b10dfcf90784",
|
"sha256:b5ed7837b923d1d71c4f587ae1539ccd96bfd6be9788f507dbe94dab5febbb5d",
|
||||||
"sha256:b8ea210810d3c14aec7561f8fe0d3eec582d1088100aaa0bb8153d53d867d20f",
|
"sha256:ba259f68250f16d2444cbbfaddaa0bb20e1560a4fdaad50bece25c199e6af864",
|
||||||
"sha256:bf572722326ce6704e863447a070039a827072b7179352570859be899b9e6551",
|
"sha256:be1d89614c6b6c36d7578496dc8625123bda2ff44f224cf8b1c45b810ee7383f",
|
||||||
"sha256:c0df57e189dacd2606cae6386acf127d01d85b2bf49acd9a65543b5d6c359ddc",
|
"sha256:c1b030a79749aa8d1f1486885040114ee56933b15ccfc90049ba266e4aa2139f",
|
||||||
"sha256:d523e75f2a8a0b4a6a8be1287c0e0e3a561b8832b05ddd987d4cd7c62f3ad3bc",
|
"sha256:c95bb147fab76f2ecde332d972d8f4138b8f2daee6c466af4ff3b4f29bd4c19e",
|
||||||
"sha256:e10593c60c5f0bfd8b241bf9f27ef2191a3005b73dde8ada0424f642543a1e59",
|
"sha256:d52c1c2d7e856cecc05aa0526453cb14574f821b7f413cc279b9514750d795c1",
|
||||||
"sha256:e9128444c83bc260aea988bf1ca6278a33ba730955bf94720468c656b61353eb",
|
"sha256:d609a6d564ad3d327e9509846c2c47f170456344521462b469e5cb39e48ba31c",
|
||||||
"sha256:f7162f2e3711f3a08a8a741f92e1f63afd58d0713177979f2cf9723dd50161cf"
|
"sha256:e1bad043c12fb58e8c7d92b3d7f2f49977dcb80a08a6d1e7a5114a11bf819fca",
|
||||||
|
"sha256:e5a675f6829c53c87d79117a8eb656cc4a5f8918185a32fc93ba09778e90f6db",
|
||||||
|
"sha256:fec32646b98baf4a22fdceb08703965bd16dea09051fbeb31a04b5b6e72b846c"
|
||||||
],
|
],
|
||||||
"version": "==5.0b1"
|
"version": "==5.0"
|
||||||
},
|
},
|
||||||
"decorator": {
|
"decorator": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -747,10 +750,10 @@
|
|||||||
},
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:48c03580720e0b46538d528b1296e4e5b24a809dcaf33a7dddec719489a9edb8",
|
"sha256:202ad3b2ec16ae7c51c02904fb838831f8d2899e61bf18db1e91a5a582feab11",
|
||||||
"sha256:6327c665c0d8721280b3036d9c9e851c60092bc1f30c8394cc433f8723e2bda5"
|
"sha256:92c84a10bec81217d9cb554ee12b3838c8986ce0b5d45f72f769da22e4bb5432"
|
||||||
],
|
],
|
||||||
"version": "==2.0.4"
|
"version": "==3.0.0"
|
||||||
},
|
},
|
||||||
"flask": {
|
"flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -791,25 +794,26 @@
|
|||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
|
"sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45",
|
||||||
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
|
"sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"
|
||||||
],
|
],
|
||||||
"version": "==1.1.0"
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
"ipdb": {
|
"ipdb": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:473fdd798a099765f093231a8b1fabfa95b0b682fce12de0c74b61a4b4d8ee57"
|
"sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.12.2"
|
"version": "==0.12.3"
|
||||||
},
|
},
|
||||||
"ipython": {
|
"ipython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51",
|
"sha256:190a279bd3d4fc585a611e9358a88f1048cc57fd688254a86f9461889ee152a6",
|
||||||
"sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e"
|
"sha256:762d79a62b6aa96b04971e920543f558dfbeedc0468b899303c080c8068d4ac2"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==7.10.1"
|
"version": "==7.10.2"
|
||||||
},
|
},
|
||||||
"ipython-genutils": {
|
"ipython-genutils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -914,30 +918,29 @@
|
|||||||
},
|
},
|
||||||
"more-itertools": {
|
"more-itertools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
|
"sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d",
|
||||||
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
|
"sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"
|
||||||
],
|
],
|
||||||
"version": "==8.0.0"
|
"version": "==8.0.2"
|
||||||
},
|
},
|
||||||
"mypy": {
|
"mypy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768",
|
"sha256:0308c35fd16c96a81b8dfc4d09ec63b8fa607cfec087acf5aafb44c2c45197de",
|
||||||
"sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646",
|
"sha256:39f7be2f89668d21b2bbab45ce5aa15e69bf8d6f3b46f9e1cc1a88e4fcc84f3d",
|
||||||
"sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c",
|
"sha256:4223f576813c79a10d0fd14192c86f1b85e3bd235c93792f22ed811a20b5ee4e",
|
||||||
"sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939",
|
"sha256:4c8f812a2fbefa96185933fbe05aa035e9cf791cf3a23bbdb6a219c80b60e0b1",
|
||||||
"sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279",
|
"sha256:4ea9ee847ea5bb38ea275441f3aea7eeba1b96187a3f968ee359d33d9dcc0eda",
|
||||||
"sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2",
|
"sha256:573c68df69f0e399fa57866a0b72989acf0a56c4008eee59c789c2ca5ea9df03",
|
||||||
"sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2",
|
"sha256:588c0e38466306aa7dbe6522ceacf37dde8b13cfa5edde90be2ce382f078875f",
|
||||||
"sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640",
|
"sha256:6d1bd2e675823a19e6bf72149540ab9851bfe698b796aea698fb926ab2bedd02",
|
||||||
"sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7",
|
"sha256:aa8e3bd1540dd5c39ef580ec2146a9c99c45f7c62af890095fec9e87b5ca19fb",
|
||||||
"sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c",
|
"sha256:b978ba1ea90d0abe2fc720ec9a41824b7d3a1304569bd58c9038d8d61dc4dfdb",
|
||||||
"sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17",
|
"sha256:c85c5367c2e8247e06cc0aba84e3633e90f48e8a0677bc51b351e138b5ff80b1",
|
||||||
"sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78",
|
"sha256:ce69577b424058bfa177df27213869f37c1e964c3e1ebd3b3d54f1d10b234c4d",
|
||||||
"sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931",
|
"sha256:ec6eaf98a57624d96d9916352a5bad2d73959f6358fabf43838f7d1a4d2f8389"
|
||||||
"sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.750"
|
"version": "==0.760"
|
||||||
},
|
},
|
||||||
"mypy-extensions": {
|
"mypy-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -948,10 +951,10 @@
|
|||||||
},
|
},
|
||||||
"parso": {
|
"parso": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc",
|
"sha256:55cf25df1a35fd88b878715874d2c4dc1ad3f0eebd1e0266a67e1f55efccfbe1",
|
||||||
"sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"
|
"sha256:5c1f7791de6bd5dbbeac8db0ef5594b36799de198b3f7f7014643b0c5536b9d3"
|
||||||
],
|
],
|
||||||
"version": "==0.5.1"
|
"version": "==0.5.2"
|
||||||
},
|
},
|
||||||
"pathspec": {
|
"pathspec": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1063,11 +1066,11 @@
|
|||||||
},
|
},
|
||||||
"pytest-mock": {
|
"pytest-mock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba",
|
"sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d",
|
||||||
"sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859"
|
"sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.12.1"
|
"version": "==1.13.0"
|
||||||
},
|
},
|
||||||
"pytest-watch": {
|
"pytest-watch": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1102,21 +1105,29 @@
|
|||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7",
|
"sha256:0472acc4b6319801c1bc681d838c88ba1446f9ae199e01f6e41091c701fb3d42",
|
||||||
"sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7",
|
"sha256:16709434c4e2332ee8ba26ae339aceb8ab0b24b8398ebd0f52ebc943f45c4fc2",
|
||||||
"sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96",
|
"sha256:223fb63ec8dcab20b3318e93dcec4aee89e98b062934090bf29ffc374d2000a2",
|
||||||
"sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1",
|
"sha256:23c3ebf05d1cd3adb26723fd598e75724e0cdb7d6a35185ac0caf061cc6edb49",
|
||||||
"sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69",
|
"sha256:2404a50fb48badaf214b700f08822b68d93d79200e0aefd9569d0332d21fbfcb",
|
||||||
"sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910",
|
"sha256:2af3a7a16fed6eff85c25da106effa36f61cbbe801d00ade349b53ce7619eb15",
|
||||||
"sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143",
|
"sha256:37e018d3746baf159aedfc9773c3cafacbd10d354ba15484f5cfc8ed9da5748b",
|
||||||
"sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59",
|
"sha256:3c9c2988d02a9238a1975c70e87c6ce94e6f36dd8e372b66f468990cfe077434",
|
||||||
"sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2",
|
"sha256:47298bc8b89d1c747f0f5974aa528fc0b6b17396f1694136a224d51461279d83",
|
||||||
"sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66",
|
"sha256:4eeb0fe936797ae00a085f99802642bfc722b3b4ea557e9e7849cb621ea10c91",
|
||||||
"sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6",
|
"sha256:6881be0218b47ed76db033f252bab3f912dfe7ed1fe7baa9daebf51de08546a0",
|
||||||
"sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a",
|
"sha256:7ac08cee5055f548eed3889e9aaef15fd00172d037949496f1f0b34acb8a7c3e",
|
||||||
"sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74"
|
"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": {
|
"requests": {
|
||||||
"hashes": [
|
"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
|
- `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
|
- `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_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_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.
|
- `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.
|
- `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,
|
ApplicationRoleStatus,
|
||||||
EnvironmentRole,
|
EnvironmentRole,
|
||||||
)
|
)
|
||||||
from atst.utils import first_or_none
|
from atst.utils import first_or_none, commit_or_raise_already_exists_error
|
||||||
|
|
||||||
|
|
||||||
class Applications(BaseDomainClass):
|
class Applications(BaseDomainClass):
|
||||||
@ -28,7 +28,7 @@ class Applications(BaseDomainClass):
|
|||||||
if environment_names:
|
if environment_names:
|
||||||
Environments.create_many(user, application, environment_names)
|
Environments.create_many(user, application, environment_names)
|
||||||
|
|
||||||
db.session.commit()
|
commit_or_raise_already_exists_error(message="application")
|
||||||
return application
|
return application
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -53,9 +53,9 @@ class Applications(BaseDomainClass):
|
|||||||
Environments.create_many(
|
Environments.create_many(
|
||||||
g.current_user, application, new_data["environment_names"]
|
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
|
return application
|
||||||
|
|
||||||
@classmethod
|
@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
|
from atst.domain.users import Users
|
||||||
|
|
||||||
@ -59,8 +59,10 @@ def get_last_login():
|
|||||||
|
|
||||||
def logout():
|
def logout():
|
||||||
if session.get("user_id"): # pragma: no branch
|
if session.get("user_id"): # pragma: no branch
|
||||||
|
dod_id = g.current_user.dod_id
|
||||||
del session["user_id"]
|
del session["user_id"]
|
||||||
del session["last_login"]
|
del session["last_login"]
|
||||||
|
app.logger.info(f"user with EDIPI {dod_id} has logged out")
|
||||||
|
|
||||||
|
|
||||||
def _unprotected_route(request):
|
def _unprotected_route(request):
|
||||||
|
@ -276,7 +276,7 @@ def existing_crl_modification_time(crl):
|
|||||||
prev_time = os.path.getmtime(crl)
|
prev_time = os.path.getmtime(crl)
|
||||||
buffered = prev_time + MODIFIED_TIME_BUFFER
|
buffered = prev_time + MODIFIED_TIME_BUFFER
|
||||||
mod_time = prev_time if pendulum.now().timestamp() < buffered else buffered
|
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")
|
return dt.format("ddd, DD MMM YYYY HH:mm:ss zz")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -6,6 +6,7 @@ from atst.models.user import User
|
|||||||
from atst.models.application import Application
|
from atst.models.application import Application
|
||||||
from atst.models.environment import Environment
|
from atst.models.environment import Environment
|
||||||
from atst.models.environment_role import EnvironmentRole
|
from atst.models.environment_role import EnvironmentRole
|
||||||
|
from .policy import AzurePolicyManager
|
||||||
|
|
||||||
|
|
||||||
class GeneralCSPException(Exception):
|
class GeneralCSPException(Exception):
|
||||||
@ -401,6 +402,7 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
|
|||||||
class AzureSDKProvider(object):
|
class AzureSDKProvider(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from azure.mgmt import subscription, authorization, managementgroups
|
from azure.mgmt import subscription, authorization, managementgroups
|
||||||
|
from azure.mgmt.resource import policy
|
||||||
import azure.graphrbac as graphrbac
|
import azure.graphrbac as graphrbac
|
||||||
import azure.common.credentials as credentials
|
import azure.common.credentials as credentials
|
||||||
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
|
||||||
@ -410,6 +412,7 @@ class AzureSDKProvider(object):
|
|||||||
self.managementgroups = managementgroups
|
self.managementgroups = managementgroups
|
||||||
self.graphrbac = graphrbac
|
self.graphrbac = graphrbac
|
||||||
self.credentials = credentials
|
self.credentials = credentials
|
||||||
|
self.policy = policy
|
||||||
# may change to a JEDI cloud
|
# may change to a JEDI cloud
|
||||||
self.cloud = AZURE_PUBLIC_CLOUD
|
self.cloud = AZURE_PUBLIC_CLOUD
|
||||||
|
|
||||||
@ -427,6 +430,8 @@ class AzureCloudProvider(CloudProviderInterface):
|
|||||||
else:
|
else:
|
||||||
self.sdk = azure_sdk_provider
|
self.sdk = azure_sdk_provider
|
||||||
|
|
||||||
|
self.policy_manager = AzurePolicyManager(config["AZURE_POLICY_LOCATION"])
|
||||||
|
|
||||||
def create_environment(
|
def create_environment(
|
||||||
self, auth_credentials: Dict, user: User, environment: 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
|
# but we just don't have a valid ID
|
||||||
pass
|
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):
|
def _get_management_service_principal(self):
|
||||||
# we really should be using graph.microsoft.com, but i'm getting
|
# we really should be using graph.microsoft.com, but i'm getting
|
||||||
# "expired token" errors for that
|
# "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(
|
||||||
return sorted(
|
"applications", []
|
||||||
[
|
)
|
||||||
cls._get_application_monthly_totals(application)
|
|
||||||
for application in applications
|
for application in portfolio.applications:
|
||||||
],
|
if application.name not in [app["name"] for app in fixture_apps]:
|
||||||
key=lambda app: app["name"],
|
fixture_apps.append({"name": application.name, "environments": []})
|
||||||
)
|
|
||||||
return []
|
return sorted(
|
||||||
|
[
|
||||||
|
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"],
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_environment_monthly_totals(cls, environment):
|
def _get_environment_monthly_totals(cls, environment):
|
||||||
@ -64,7 +72,7 @@ class MockReportingProvider:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@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
|
returns a dictionary that represents spending totals for an application
|
||||||
and its environments e.g.
|
and its environments e.g.
|
||||||
@ -83,19 +91,28 @@ class MockReportingProvider:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
environments = sorted(
|
application_envs = [
|
||||||
[
|
env
|
||||||
cls._get_environment_monthly_totals(env)
|
for env in portfolio.all_environments
|
||||||
for env in application["environments"]
|
if env.application.name == fixture_app["name"]
|
||||||
],
|
]
|
||||||
key=lambda env: env["name"],
|
|
||||||
)
|
environments = [
|
||||||
|
cls._get_environment_monthly_totals(env)
|
||||||
|
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 {
|
return {
|
||||||
"name": application["name"],
|
"name": fixture_app["name"],
|
||||||
"this_month": sum(env["this_month"] for env in environments),
|
"this_month": sum(env.get("this_month", 0) for env in environments),
|
||||||
"last_month": sum(env["last_month"] for env in environments),
|
"last_month": sum(env.get("last_month", 0) for env in environments),
|
||||||
"total": sum(env["total"] for env in environments),
|
"total": sum(env.get("total", 0) for env in environments),
|
||||||
"environments": environments,
|
"environments": sorted(environments, key=lambda env: env["name"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -12,6 +12,7 @@ from atst.models import (
|
|||||||
CLIN,
|
CLIN,
|
||||||
)
|
)
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
|
from atst.utils import commit_or_raise_already_exists_error
|
||||||
|
|
||||||
from .exceptions import NotFoundError, DisabledError
|
from .exceptions import NotFoundError, DisabledError
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ class Environments(object):
|
|||||||
def create(cls, user, application, name):
|
def create(cls, user, application, name):
|
||||||
environment = Environment(application=application, name=name, creator=user)
|
environment = Environment(application=application, name=name, creator=user)
|
||||||
db.session.add(environment)
|
db.session.add(environment)
|
||||||
db.session.commit()
|
commit_or_raise_already_exists_error(message="environment")
|
||||||
return environment
|
return environment
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -39,7 +40,8 @@ class Environments(object):
|
|||||||
if name is not None:
|
if name is not None:
|
||||||
environment.name = name
|
environment.name = name
|
||||||
db.session.add(environment)
|
db.session.add(environment)
|
||||||
db.session.commit()
|
commit_or_raise_already_exists_error(message="environment")
|
||||||
|
return environment
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, environment_id):
|
def get(cls, environment_id):
|
||||||
|
@ -75,10 +75,10 @@ class Portfolios(object):
|
|||||||
permission_sets = PortfolioRoles._permission_sets_for_names(
|
permission_sets = PortfolioRoles._permission_sets_for_names(
|
||||||
member_data.get("permission_sets", [])
|
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(
|
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)
|
PortfoliosQuery.add_and_commit(role)
|
||||||
@ -107,4 +107,7 @@ class Portfolios(object):
|
|||||||
if "name" in new_data:
|
if "name" in new_data:
|
||||||
portfolio.name = new_data["name"]
|
portfolio.name = new_data["name"]
|
||||||
|
|
||||||
|
if "description" in new_data:
|
||||||
|
portfolio.description = new_data["description"]
|
||||||
|
|
||||||
PortfoliosQuery.add_and_commit(portfolio)
|
PortfoliosQuery.add_and_commit(portfolio)
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.clin import CLIN
|
from atst.models.clin import CLIN
|
||||||
from atst.models.task_order import TaskOrder, SORT_ORDERING
|
from atst.models.task_order import TaskOrder, SORT_ORDERING
|
||||||
from . import BaseDomainClass
|
from . import BaseDomainClass
|
||||||
from .exceptions import AlreadyExistsError
|
from atst.utils import commit_or_raise_already_exists_error
|
||||||
|
|
||||||
|
|
||||||
class TaskOrders(BaseDomainClass):
|
class TaskOrders(BaseDomainClass):
|
||||||
@ -16,15 +15,8 @@ class TaskOrders(BaseDomainClass):
|
|||||||
def create(cls, portfolio_id, number, clins, pdf):
|
def create(cls, portfolio_id, number, clins, pdf):
|
||||||
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
|
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
|
commit_or_raise_already_exists_error(message="task_order")
|
||||||
try:
|
|
||||||
db.session.commit()
|
|
||||||
except IntegrityError:
|
|
||||||
db.session.rollback()
|
|
||||||
raise AlreadyExistsError("task_order")
|
|
||||||
|
|
||||||
TaskOrders.create_clins(task_order.id, clins)
|
TaskOrders.create_clins(task_order.id, clins)
|
||||||
|
|
||||||
return task_order
|
return task_order
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -42,12 +34,7 @@ class TaskOrders(BaseDomainClass):
|
|||||||
task_order.number = number
|
task_order.number = number
|
||||||
db.session.add(task_order)
|
db.session.add(task_order)
|
||||||
|
|
||||||
try:
|
commit_or_raise_already_exists_error(message="task_order")
|
||||||
db.session.commit()
|
|
||||||
except IntegrityError:
|
|
||||||
db.session.rollback()
|
|
||||||
raise AlreadyExistsError("task_order")
|
|
||||||
|
|
||||||
return task_order
|
return task_order
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -117,12 +117,3 @@ class Users(object):
|
|||||||
user.last_session_id = session_id
|
user.last_session_id = session_id
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
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"),)
|
description = TextAreaField(translate("forms.portfolio.description.label"),)
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioCreationForm(PortfolioForm):
|
||||||
defense_component = SelectMultipleField(
|
defense_component = SelectMultipleField(
|
||||||
choices=SERVICE_BRANCHES,
|
choices=SERVICE_BRANCHES,
|
||||||
widget=ListWidget(prefix_label=False),
|
widget=ListWidget(prefix_label=False),
|
||||||
|
@ -1,76 +1,59 @@
|
|||||||
from wtforms.validators import Required
|
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 .forms import BaseForm
|
||||||
from .member import NewForm as BaseNewMemberForm
|
from .member import NewForm as BaseNewMemberForm
|
||||||
|
from atst.domain.permission_sets import PermissionSets
|
||||||
from atst.forms.fields import SelectField
|
from atst.forms.fields import SelectField
|
||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
class PermissionsForm(BaseForm):
|
class PermissionsForm(BaseForm):
|
||||||
member_name = StringField()
|
perms_app_mgmt = BooleanField(
|
||||||
member_id = HiddenField()
|
translate("forms.new_member.app_mgmt.label"),
|
||||||
perms_app_mgmt = SelectField(
|
default=False,
|
||||||
translate("forms.new_member.app_mgmt"),
|
description=translate("forms.new_member.app_mgmt.description"),
|
||||||
choices=[
|
|
||||||
(
|
|
||||||
PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT,
|
|
||||||
translate("common.view"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT,
|
|
||||||
translate("common.edit"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
perms_funding = SelectField(
|
perms_funding = BooleanField(
|
||||||
translate("forms.new_member.funding"),
|
translate("forms.new_member.funding.label"),
|
||||||
choices=[
|
default=False,
|
||||||
(PermissionSets.VIEW_PORTFOLIO_FUNDING, translate("common.view")),
|
description=translate("forms.new_member.funding.description"),
|
||||||
(PermissionSets.EDIT_PORTFOLIO_FUNDING, translate("common.edit")),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
perms_reporting = SelectField(
|
perms_reporting = BooleanField(
|
||||||
translate("forms.new_member.reporting"),
|
translate("forms.new_member.reporting.label"),
|
||||||
choices=[
|
default=False,
|
||||||
(PermissionSets.VIEW_PORTFOLIO_REPORTS, translate("common.view")),
|
description=translate("forms.new_member.reporting.description"),
|
||||||
(PermissionSets.EDIT_PORTFOLIO_REPORTS, translate("common.edit")),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
perms_portfolio_mgmt = SelectField(
|
perms_portfolio_mgmt = BooleanField(
|
||||||
translate("forms.new_member.portfolio_mgmt"),
|
translate("forms.new_member.portfolio_mgmt.label"),
|
||||||
choices=[
|
default=False,
|
||||||
(PermissionSets.VIEW_PORTFOLIO_ADMIN, translate("common.view")),
|
description=translate("forms.new_member.portfolio_mgmt.description"),
|
||||||
(PermissionSets.EDIT_PORTFOLIO_ADMIN, translate("common.edit")),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
_data = super().data
|
_data = super().data
|
||||||
_data["permission_sets"] = []
|
_data.pop("csrf_token", None)
|
||||||
for field in _data:
|
perm_sets = []
|
||||||
if "perms" in field:
|
|
||||||
_data["permission_sets"].append(_data[field])
|
|
||||||
|
|
||||||
|
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
|
return _data
|
||||||
|
|
||||||
|
|
||||||
class MembersPermissionsForm(BaseForm):
|
class NewForm(PermissionsForm):
|
||||||
members_permissions = FieldList(FormField(PermissionsForm))
|
|
||||||
|
|
||||||
|
|
||||||
class NewForm(BaseForm):
|
|
||||||
user_data = FormField(BaseNewMemberForm)
|
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):
|
class AssignPPOCForm(PermissionsForm):
|
||||||
|
@ -151,3 +151,6 @@ class SignatureForm(BaseForm):
|
|||||||
translate("task_orders.sign.digital_signature_description"),
|
translate("task_orders.sign.digital_signature_description"),
|
||||||
validators=[Required()],
|
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 sqlalchemy.orm import relationship, synonym
|
||||||
|
|
||||||
from atst.models.base import Base
|
from atst.models.base import Base
|
||||||
@ -34,6 +34,11 @@ class Application(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
members = synonym("roles")
|
members = synonym("roles")
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"name", "portfolio_id", name="applications_name_portfolio_id_key"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users(self):
|
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.orm import relationship
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@ -38,6 +38,12 @@ class Environment(
|
|||||||
primaryjoin="and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)",
|
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):
|
class ProvisioningStatus(Enum):
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy import Column, String
|
from sqlalchemy import Column, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.types import ARRAY
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from atst.models.base import Base
|
from atst.models.base import Base
|
||||||
@ -20,7 +21,7 @@ class Portfolio(
|
|||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
defense_component = Column(
|
defense_component = Column(
|
||||||
String, nullable=False
|
ARRAY(String), nullable=False
|
||||||
) # Department of Defense Component
|
) # Department of Defense Component
|
||||||
|
|
||||||
applications = relationship(
|
applications = relationship(
|
||||||
|
@ -12,17 +12,6 @@ from atst.utils import first_or_none
|
|||||||
from atst.models.mixins.auditable import record_permission_sets_updates
|
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):
|
class Status(Enum):
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
DISABLED = "disabled"
|
DISABLED = "disabled"
|
||||||
@ -90,23 +79,23 @@ class PortfolioRole(
|
|||||||
@property
|
@property
|
||||||
def display_status(self):
|
def display_status(self):
|
||||||
if self.status == Status.ACTIVE:
|
if self.status == Status.ACTIVE:
|
||||||
return MEMBER_STATUSES["active"]
|
return "active"
|
||||||
elif self.status == Status.DISABLED:
|
elif self.status == Status.DISABLED:
|
||||||
return MEMBER_STATUSES["disabled"]
|
return "disabled"
|
||||||
elif self.latest_invitation:
|
elif self.latest_invitation:
|
||||||
if self.latest_invitation.is_revoked:
|
if self.latest_invitation.is_revoked:
|
||||||
return MEMBER_STATUSES["revoked"]
|
return "invite_revoked"
|
||||||
elif self.latest_invitation.is_rejected_wrong_user:
|
elif self.latest_invitation.is_rejected_wrong_user:
|
||||||
return MEMBER_STATUSES["error"]
|
return "invite_error"
|
||||||
elif (
|
elif (
|
||||||
self.latest_invitation.is_rejected_expired
|
self.latest_invitation.is_rejected_expired
|
||||||
or self.latest_invitation.is_expired
|
or self.latest_invitation.is_expired
|
||||||
):
|
):
|
||||||
return MEMBER_STATUSES["expired"]
|
return "invite_expired"
|
||||||
else:
|
else:
|
||||||
return MEMBER_STATUSES["pending"]
|
return "invite_pending"
|
||||||
else:
|
else:
|
||||||
return MEMBER_STATUSES["unknown"]
|
return "unknown"
|
||||||
|
|
||||||
def has_permission_set(self, perm_set_name):
|
def has_permission_set(self, perm_set_name):
|
||||||
return first_or_none(
|
return first_or_none(
|
||||||
|
@ -9,7 +9,7 @@ from atst.models.base import Base
|
|||||||
import atst.models.types as types
|
import atst.models.types as types
|
||||||
import atst.models.mixins as mixins
|
import atst.models.mixins as mixins
|
||||||
from atst.models.attachment import Attachment
|
from atst.models.attachment import Attachment
|
||||||
from atst.utils.clock import Clock
|
from pendulum import today
|
||||||
|
|
||||||
|
|
||||||
class Status(Enum):
|
class Status(Enum):
|
||||||
@ -83,26 +83,10 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
def is_active(self):
|
def is_active(self):
|
||||||
return self.status == Status.ACTIVE
|
return self.status == Status.ACTIVE
|
||||||
|
|
||||||
@property
|
|
||||||
def is_upcoming(self):
|
|
||||||
return self.status == Status.UPCOMING
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
return self.status == Status.EXPIRED
|
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
|
@property
|
||||||
def clins_are_completed(self):
|
def clins_are_completed(self):
|
||||||
return all([len(self.clins), (clin.is_completed for clin in self.clins)])
|
return all([len(self.clins), (clin.is_completed for clin in self.clins)])
|
||||||
@ -117,17 +101,17 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self):
|
def status(self):
|
||||||
today = Clock.today()
|
todays_date = today(tz="UTC").date()
|
||||||
|
|
||||||
if not self.is_completed and not self.is_signed:
|
if not self.is_completed and not self.is_signed:
|
||||||
return Status.DRAFT
|
return Status.DRAFT
|
||||||
elif self.is_completed and not self.is_signed:
|
elif self.is_completed and not self.is_signed:
|
||||||
return Status.UNSIGNED
|
return Status.UNSIGNED
|
||||||
elif today < self.start_date:
|
elif todays_date < self.start_date:
|
||||||
return Status.UPCOMING
|
return Status.UPCOMING
|
||||||
elif today >= self.end_date:
|
elif todays_date > self.end_date:
|
||||||
return Status.EXPIRED
|
return Status.EXPIRED
|
||||||
elif self.start_date <= today < self.end_date:
|
elif self.start_date <= todays_date <= self.end_date:
|
||||||
return Status.ACTIVE
|
return Status.ACTIVE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -141,39 +125,25 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
@property
|
@property
|
||||||
def days_to_expiration(self):
|
def days_to_expiration(self):
|
||||||
if self.end_date:
|
if self.end_date:
|
||||||
return (self.end_date - Clock.today()).days
|
return (self.end_date - today(tz="UTC").date()).days
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_obligated_funds(self):
|
def total_obligated_funds(self):
|
||||||
total = 0
|
return sum(
|
||||||
for clin in self.clins:
|
(clin.obligated_amount for clin in self.clins if clin.obligated_amount)
|
||||||
if clin.obligated_amount is not None:
|
)
|
||||||
total += clin.obligated_amount
|
|
||||||
return total
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_contract_amount(self):
|
def total_contract_amount(self):
|
||||||
total = 0
|
return sum((clin.total_amount for clin in self.clins if clin.total_amount))
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invoiced_funds(self):
|
def invoiced_funds(self):
|
||||||
# TODO: implement this using reporting data from the CSP
|
# TODO: implement this using reporting data from the CSP
|
||||||
return self.total_obligated_funds * Decimal(0.75)
|
if self.is_active:
|
||||||
|
return self.total_obligated_funds * Decimal(0.75)
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_status(self):
|
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.orm import relationship
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
from sqlalchemy.event import listen
|
from sqlalchemy.event import listen
|
||||||
@ -67,8 +67,6 @@ class User(
|
|||||||
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
|
last_login = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||||
last_session_id = Column(UUID(as_uuid=True), nullable=True)
|
last_session_id = Column(UUID(as_uuid=True), nullable=True)
|
||||||
|
|
||||||
provisional = Column(Boolean)
|
|
||||||
|
|
||||||
cloud_id = Column(String)
|
cloud_id = Column(String)
|
||||||
|
|
||||||
REQUIRED_FIELDS = [
|
REQUIRED_FIELDS = [
|
||||||
|
@ -19,6 +19,7 @@ from werkzeug.exceptions import NotFound
|
|||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
from atst.domain.authnid import AuthenticationContext
|
from atst.domain.authnid import AuthenticationContext
|
||||||
from atst.domain.auth import logout as _logout
|
from atst.domain.auth import logout as _logout
|
||||||
|
from atst.domain.exceptions import UnauthenticatedError
|
||||||
from atst.utils.flash import formatted_flash as flash
|
from atst.utils.flash import formatted_flash as flash
|
||||||
|
|
||||||
|
|
||||||
@ -64,11 +65,15 @@ def catch_all(path):
|
|||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
|
|
||||||
|
def _client_s_dn():
|
||||||
|
return request.environ.get("HTTP_X_SSL_CLIENT_S_DN")
|
||||||
|
|
||||||
|
|
||||||
def _make_authentication_context():
|
def _make_authentication_context():
|
||||||
return AuthenticationContext(
|
return AuthenticationContext(
|
||||||
crl_cache=app.crl_cache,
|
crl_cache=app.crl_cache,
|
||||||
auth_status=request.environ.get("HTTP_X_SSL_CLIENT_VERIFY"),
|
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"),
|
cert=request.environ.get("HTTP_X_SSL_CLIENT_CERT"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,19 +94,24 @@ def current_user_setup(user):
|
|||||||
session["user_id"] = user.id
|
session["user_id"] = user.id
|
||||||
session["last_login"] = user.last_login
|
session["last_login"] = user.last_login
|
||||||
app.session_limiter.on_login(user)
|
app.session_limiter.on_login(user)
|
||||||
|
app.logger.info(f"authentication succeeded for user with EDIPI {user.dod_id}")
|
||||||
Users.update_last_login(user)
|
Users.update_last_login(user)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/login-redirect")
|
@bp.route("/login-redirect")
|
||||||
def login_redirect():
|
def login_redirect():
|
||||||
auth_context = _make_authentication_context()
|
try:
|
||||||
auth_context.authenticate()
|
auth_context = _make_authentication_context()
|
||||||
user = auth_context.get_user()
|
auth_context.authenticate()
|
||||||
|
|
||||||
if user.provisional:
|
user = auth_context.get_user()
|
||||||
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
|
||||||
|
|
||||||
current_user_setup(user)
|
|
||||||
return redirect(redirect_after_login_url())
|
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 .blueprint import applications_bp
|
||||||
from atst.domain.applications import Applications
|
from atst.domain.applications import Applications
|
||||||
from atst.domain.portfolios import Portfolios
|
|
||||||
from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
|
from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
|
||||||
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
||||||
from atst.models.permissions import Permissions
|
from atst.models.permissions import Permissions
|
||||||
@ -12,6 +11,7 @@ from atst.routes.applications.settings import (
|
|||||||
get_new_member_form,
|
get_new_member_form,
|
||||||
handle_create_member,
|
handle_create_member,
|
||||||
handle_update_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(
|
form = get_new_application_form(
|
||||||
{**http_request.form}, NameAndDescriptionForm, application_id
|
{**http_request.form}, NameAndDescriptionForm, application_id
|
||||||
)
|
)
|
||||||
|
application = handle_update_application(form, application_id, portfolio_id)
|
||||||
|
|
||||||
if form.validate():
|
if application:
|
||||||
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)
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"applications.update_new_application_step_2",
|
"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 .blueprint import applications_bp
|
||||||
from atst.domain.exceptions import AlreadyExistsError
|
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.common import Paginator
|
||||||
from atst.domain.environment_roles import EnvironmentRoles
|
from atst.domain.environment_roles import EnvironmentRoles
|
||||||
from atst.domain.invitations import ApplicationInvitations
|
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_member import NewForm as NewMemberForm, UpdateMemberForm
|
||||||
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
|
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
|
||||||
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
|
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
|
# 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")
|
@applications_bp.route("/applications/<application_id>/settings")
|
||||||
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
|
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
|
||||||
def settings(application_id):
|
def settings(application_id):
|
||||||
application = Applications.get(application_id)
|
application = Applications.get(application_id)
|
||||||
|
|
||||||
return render_settings_page(
|
return render_settings_page(application=application,)
|
||||||
application=application,
|
|
||||||
active_toggler=http_request.args.get("active_toggler"),
|
|
||||||
active_toggler_section=http_request.args.get("active_toggler_section"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
|
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
|
||||||
@ -264,31 +314,21 @@ def update_environment(environment_id):
|
|||||||
application = environment.application
|
application = environment.application
|
||||||
|
|
||||||
env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form)
|
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():
|
if updated_environment:
|
||||||
Environments.update(environment=environment, name=env_form.name.data)
|
|
||||||
|
|
||||||
flash("application_environments_updated")
|
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"applications.settings",
|
"applications.settings",
|
||||||
application_id=application.id,
|
application_id=application.id,
|
||||||
fragment="application-environments",
|
fragment="application-environments",
|
||||||
_anchor="application-environments",
|
_anchor="application-environments",
|
||||||
active_toggler=environment.id,
|
|
||||||
active_toggler_section="edit",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return (
|
return (render_settings_page(application=application, show_flash=True), 400)
|
||||||
render_settings_page(
|
|
||||||
application=application,
|
|
||||||
active_toggler=environment.id,
|
|
||||||
active_toggler_section="edit",
|
|
||||||
),
|
|
||||||
400,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@applications_bp.route(
|
@applications_bp.route(
|
||||||
@ -298,14 +338,9 @@ def update_environment(environment_id):
|
|||||||
def new_environment(application_id):
|
def new_environment(application_id):
|
||||||
application = Applications.get(application_id)
|
application = Applications.get(application_id)
|
||||||
env_form = EditEnvironmentForm(formdata=http_request.form)
|
env_form = EditEnvironmentForm(formdata=http_request.form)
|
||||||
|
environment = handle_update_environment(form=env_form, application=application)
|
||||||
|
|
||||||
if env_form.validate():
|
if environment:
|
||||||
Environments.create(
|
|
||||||
g.current_user, application=application, name=env_form.name.data
|
|
||||||
)
|
|
||||||
|
|
||||||
flash("environment_added", environment_name=env_form.data["name"])
|
|
||||||
|
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"applications.settings",
|
"applications.settings",
|
||||||
@ -315,7 +350,7 @@ def new_environment(application_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
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"])
|
@applications_bp.route("/applications/<application_id>/edit", methods=["POST"])
|
||||||
@ -323,10 +358,9 @@ def new_environment(application_id):
|
|||||||
def update(application_id):
|
def update(application_id):
|
||||||
application = Applications.get(application_id)
|
application = Applications.get(application_id)
|
||||||
form = NameAndDescriptionForm(http_request.form)
|
form = NameAndDescriptionForm(http_request.form)
|
||||||
if form.validate():
|
updated_application = handle_update_application(form, application_id)
|
||||||
application_data = form.data
|
|
||||||
Applications.update(application, application_data)
|
|
||||||
|
|
||||||
|
if updated_application:
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"applications.portfolio_applications",
|
"applications.portfolio_applications",
|
||||||
@ -334,22 +368,10 @@ def update(application_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return render_settings_page(application=application, application_form=form)
|
return (
|
||||||
|
render_settings_page(application=application, show_flash=True),
|
||||||
|
400,
|
||||||
@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
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@applications_bp.route("/environments/<environment_id>/delete", methods=["POST"])
|
@applications_bp.route("/environments/<environment_id>/delete", methods=["POST"])
|
||||||
|
@ -17,63 +17,51 @@ from atst.utils.flash import formatted_flash as flash
|
|||||||
from atst.domain.exceptions import UnauthorizedError
|
from atst.domain.exceptions import UnauthorizedError
|
||||||
|
|
||||||
|
|
||||||
def permission_str(member, edit_perm_set, view_perm_set):
|
def filter_perm_sets_data(member):
|
||||||
if member.has_permission_set(edit_perm_set):
|
perm_sets_data = {
|
||||||
return edit_perm_set
|
"perms_portfolio_mgmt": bool(
|
||||||
else:
|
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_ADMIN)
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
"perms_funding": permission_str(
|
"perms_app_mgmt": bool(
|
||||||
member,
|
member.has_permission_set(
|
||||||
PermissionSets.EDIT_PORTFOLIO_FUNDING,
|
PermissionSets.EDIT_PORTFOLIO_APPLICATION_MANAGEMENT
|
||||||
PermissionSets.VIEW_PORTFOLIO_FUNDING,
|
)
|
||||||
),
|
),
|
||||||
"perms_reporting": permission_str(
|
"perms_funding": bool(
|
||||||
member,
|
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_FUNDING)
|
||||||
PermissionSets.EDIT_PORTFOLIO_REPORTS,
|
|
||||||
PermissionSets.VIEW_PORTFOLIO_REPORTS,
|
|
||||||
),
|
),
|
||||||
"perms_portfolio_mgmt": permission_str(
|
"perms_reporting": bool(
|
||||||
member,
|
member.has_permission_set(PermissionSets.EDIT_PORTFOLIO_REPORTS)
|
||||||
PermissionSets.EDIT_PORTFOLIO_ADMIN,
|
|
||||||
PermissionSets.VIEW_PORTFOLIO_ADMIN,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return perm_sets_data
|
||||||
|
|
||||||
def get_members_data(portfolio):
|
|
||||||
members = sorted(
|
def filter_members_data(members_list, portfolio):
|
||||||
[serialize_member_form_data(member) for member in portfolio.members],
|
members_data = []
|
||||||
key=lambda member: member["member_name"],
|
for member in members_list:
|
||||||
)
|
members_data.append(
|
||||||
for member in members:
|
{
|
||||||
if member["member_id"] == portfolio.owner_role.id:
|
"role_id": member.id,
|
||||||
ppoc = member
|
"user_name": member.user_name,
|
||||||
members.remove(member)
|
"permission_sets": filter_perm_sets_data(member),
|
||||||
members.insert(0, ppoc)
|
"status": member.display_status,
|
||||||
return members
|
"ppoc": PermissionSets.PORTFOLIO_POC in member.permission_sets,
|
||||||
|
# add in stuff here for forms
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted(members_data, key=lambda member: member["user_name"])
|
||||||
|
|
||||||
|
|
||||||
def render_admin_page(portfolio, form=None):
|
def render_admin_page(portfolio, form=None):
|
||||||
pagination_opts = Paginator.get_pagination_opts(http_request)
|
pagination_opts = Paginator.get_pagination_opts(http_request)
|
||||||
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
|
audit_events = AuditLog.get_portfolio_events(portfolio, pagination_opts)
|
||||||
members_data = get_members_data(portfolio)
|
portfolio_form = PortfolioForm(obj=portfolio)
|
||||||
portfolio_form = PortfolioForm(data={"name": portfolio.name})
|
member_list = portfolio.members
|
||||||
member_perms_form = member_forms.MembersPermissionsForm(
|
|
||||||
data={"members_permissions": members_data}
|
|
||||||
)
|
|
||||||
|
|
||||||
assign_ppoc_form = member_forms.AssignPPOCForm()
|
assign_ppoc_form = member_forms.AssignPPOCForm()
|
||||||
|
|
||||||
for pf_role in portfolio.roles:
|
for pf_role in portfolio.roles:
|
||||||
if pf_role.user != portfolio.owner and pf_role.is_active:
|
if pf_role.user != portfolio.owner and pf_role.is_active:
|
||||||
assign_ppoc_form.role_id.choices += [(pf_role.id, pf_role.full_name)]
|
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",
|
"portfolios/admin.html",
|
||||||
form=form,
|
form=form,
|
||||||
portfolio_form=portfolio_form,
|
portfolio_form=portfolio_form,
|
||||||
member_perms_form=member_perms_form,
|
members=filter_members_data(member_list, portfolio),
|
||||||
member_form=member_forms.NewForm(),
|
new_manager_form=member_forms.NewForm(),
|
||||||
assign_ppoc_form=assign_ppoc_form,
|
assign_ppoc_form=assign_ppoc_form,
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
audit_events=audit_events,
|
audit_events=audit_events,
|
||||||
user=g.current_user,
|
user=g.current_user,
|
||||||
ppoc_id=members_data[0].get("member_id"),
|
|
||||||
current_member_id=current_member_id,
|
current_member_id=current_member_id,
|
||||||
applications_count=len(portfolio.applications),
|
applications_count=len(portfolio.applications),
|
||||||
)
|
)
|
||||||
@ -106,34 +93,6 @@ def admin(portfolio_id):
|
|||||||
return render_admin_page(portfolio)
|
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"])
|
@portfolios_bp.route("/portfolios/<portfolio_id>/update_ppoc", methods=["POST"])
|
||||||
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
|
@user_can(Permissions.EDIT_PORTFOLIO_POC, message="update portfolio ppoc")
|
||||||
def update_ppoc(portfolio_id):
|
def update_ppoc(portfolio_id):
|
||||||
|
@ -56,13 +56,3 @@ def reports(portfolio_id):
|
|||||||
monthly_spending=Reports.monthly_spending(portfolio),
|
monthly_spending=Reports.monthly_spending(portfolio),
|
||||||
retrieved=datetime.now(), # mocked datetime of reporting data retrival
|
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():
|
if form.validate():
|
||||||
try:
|
try:
|
||||||
invite = Portfolios.invite(portfolio, g.current_user, form.update_data)
|
invite = Portfolios.invite(portfolio, g.current_user, form.data)
|
||||||
send_portfolio_invitation(
|
send_portfolio_invitation(
|
||||||
invitee_email=invite.email,
|
invitee_email=invite.email,
|
||||||
inviter_name=g.current_user.full_name,
|
inviter_name=g.current_user.full_name,
|
||||||
|
@ -8,16 +8,16 @@ from atst.forms.task_order import SignatureForm
|
|||||||
from atst.models import Permissions
|
from atst.models import Permissions
|
||||||
|
|
||||||
|
|
||||||
@task_orders_bp.route("/task_orders/<task_order_id>/review")
|
@task_orders_bp.route("/task_orders/<task_order_id>")
|
||||||
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="review task order details")
|
@user_can(Permissions.VIEW_TASK_ORDER_DETAILS, message="view task order details")
|
||||||
def review_task_order(task_order_id):
|
def view_task_order(task_order_id):
|
||||||
task_order = TaskOrders.get(task_order_id)
|
task_order = TaskOrders.get(task_order_id)
|
||||||
if task_order.is_draft:
|
if task_order.is_draft:
|
||||||
return redirect(url_for("task_orders.edit", task_order_id=task_order.id))
|
return redirect(url_for("task_orders.edit", task_order_id=task_order.id))
|
||||||
else:
|
else:
|
||||||
signature_form = SignatureForm()
|
signature_form = SignatureForm()
|
||||||
return render_template(
|
return render_template(
|
||||||
"task_orders/review.html",
|
"task_orders/view.html",
|
||||||
task_order=task_order,
|
task_order=task_order,
|
||||||
signature_form=signature_form,
|
signature_form=signature_form,
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from atst.database import db
|
||||||
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
|
|
||||||
|
|
||||||
def first_or_none(predicate, lst):
|
def first_or_none(predicate, lst):
|
||||||
return next((x for x in lst if predicate(x)), None)
|
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):
|
def pick(keys, dct):
|
||||||
_keys = set(keys)
|
_keys = set(keys)
|
||||||
return {k: v for (k, v) in dct.items() if k in _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
|
from atst.utils.localization import translate
|
||||||
|
|
||||||
|
|
||||||
MESSAGES = {
|
MESSAGES = {
|
||||||
"portfolio_deleted": {
|
|
||||||
"title_template": "Portfolio has been deleted",
|
|
||||||
"message_template": "Portfolio '{{portfolio_name}}' has been deleted",
|
|
||||||
"category": "success",
|
|
||||||
},
|
|
||||||
"application_created": {
|
"application_created": {
|
||||||
"title_template": translate("flash.application.created.title"),
|
"title": "flash.application.created.title",
|
||||||
"message_template": """
|
"message": "flash.application.created.message",
|
||||||
{{ "flash.application.created.message" | translate({"application_name": application_name}) }}
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"application_updated": {
|
"application_updated": {
|
||||||
"title_template": translate("flash.success"),
|
"title": "flash.success",
|
||||||
"message_template": """
|
"message": "flash.application.updated",
|
||||||
{{ "flash.application.updated" | translate({"application_name": application_name}) }}
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"application_deleted": {
|
"application_environments_name_error": {
|
||||||
"title_template": translate("flash.success"),
|
"title": None,
|
||||||
"message_template": """
|
"message": "flash.application.env_name_error.message",
|
||||||
{{ "flash.application.deleted" | translate({"application_name": application_name}) }}
|
"category": "error",
|
||||||
<a href="#">{{ "common.undo" | translate }}</a>
|
|
||||||
""",
|
|
||||||
"category": "success",
|
|
||||||
},
|
},
|
||||||
"application_environments_updated": {
|
"application_environments_updated": {
|
||||||
"title_template": "Application environments updated",
|
"title": "flash.environment.updated.title",
|
||||||
"message_template": "Application environments have been updated",
|
"message": "flash.environment.updated.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"application_invite_error": {
|
"application_invite_error": {
|
||||||
"title_template": "Application invitation error",
|
"title": "flash.application_invite.error.title",
|
||||||
"message_template": "There was an error processing the invitation for {{ user_name }} from {{ application_name }}",
|
"message": "flash.application_invite.error.message",
|
||||||
"category": "error",
|
"category": "error",
|
||||||
},
|
},
|
||||||
"application_invite_resent": {
|
"application_invite_resent": {
|
||||||
"title_template": "Application invitation resent",
|
"title": "flash.application_invite.resent.title",
|
||||||
"message_template": "You have successfully resent the invite for {{ user_name }} from {{ application_name }}",
|
"message": "flash.application_invite.resent.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"application_invite_revoked": {
|
"application_invite_revoked": {
|
||||||
"title_template": "Application invitation revoked",
|
"title": "flash.application_invite.revoked.title",
|
||||||
"message_template": "You have successfully revoked the invite for {{ user_name }} from {{ application_name }}",
|
"message": "flash.application_invite.revoked.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"application_member_removed": {
|
"application_member_removed": {
|
||||||
"title_template": "Team member removed from application",
|
"title": "flash.application_member.removed.title",
|
||||||
"message_template": "You have successfully deleted {{ user_name }} from {{ application_name }}",
|
"message": "flash.application_member.removed.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"application_member_update_error": {
|
"application_member_update_error": {
|
||||||
"title_template": "{{ user_name }} could not be updated",
|
"title": "flash.application_member.update_error.title",
|
||||||
"message_template": "An unexpected problem occurred with your request, please try again. If the problem persists, contact an administrator.",
|
"message": "flash.application_member.update_error.message",
|
||||||
"category": "error",
|
"category": "error",
|
||||||
},
|
},
|
||||||
"application_member_updated": {
|
"application_member_updated": {
|
||||||
"title_template": "Team member updated",
|
"title": "flash.application_member.updated.title",
|
||||||
"message_template": "You have successfully updated the permissions for {{ user_name }}",
|
"message": "flash.application_member.updated.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
|
"application_name_error": {
|
||||||
|
"title": None,
|
||||||
|
"message": "flash.application.name_error.message",
|
||||||
|
"category": "error",
|
||||||
|
},
|
||||||
"ccpo_user_added": {
|
"ccpo_user_added": {
|
||||||
"title_template": translate("flash.success"),
|
"title": "flash.success",
|
||||||
"message_template": "You have successfully given {{ user_name }} CCPO permissions.",
|
"message": "flash.ccpo_user.added.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"ccpo_user_not_found": {
|
"ccpo_user_not_found": {
|
||||||
"title_template": translate("ccpo.form.user_not_found_title"),
|
"title": "ccpo.form.user_not_found_title",
|
||||||
"message_template": translate("ccpo.form.user_not_found_text"),
|
"message": "ccpo.form.user_not_found_text",
|
||||||
"category": "info",
|
"category": "info",
|
||||||
},
|
},
|
||||||
"ccpo_user_removed": {
|
"ccpo_user_removed": {
|
||||||
"title_template": translate("flash.success"),
|
"title": "flash.success",
|
||||||
"message_template": "You have successfully removed {{ user_name }}'s CCPO permissions.",
|
"message": "flash.ccpo_user.removed.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"environment_added": {
|
"environment_added": {
|
||||||
"title_template": translate("flash.success"),
|
"title": "flash.success",
|
||||||
"message_template": """
|
"message": "flash.environment_added",
|
||||||
{{ "flash.environment_added" | translate({ "env_name": environment_name }) }}
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"environment_deleted": {
|
"environment_deleted": {
|
||||||
"title_template": "{{ environment_name }} deleted",
|
"title": "flash.environment.deleted.title",
|
||||||
"message_template": 'The environment "{{ environment_name }}" has been deleted',
|
"message": "flash.environment.deleted.message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"form_errors": {
|
"form_errors": {
|
||||||
"title_template": "There were some errors",
|
"title": "flash.form.errors.title",
|
||||||
"message_template": "<p>Please see below.</p>",
|
"message": "flash.form.errors.message",
|
||||||
"category": "error",
|
"category": "error",
|
||||||
},
|
},
|
||||||
"insufficient_funds": {
|
"insufficient_funds": {
|
||||||
"title_template": "Insufficient Funds",
|
"title": "flash.task_order.insufficient_funds.title",
|
||||||
"message_template": "",
|
"message": "",
|
||||||
"category": "warning",
|
"category": "warning",
|
||||||
},
|
},
|
||||||
"logged_out": {
|
"logged_out": {
|
||||||
"title_template": translate("flash.logged_out"),
|
"title": "flash.logged_out.title",
|
||||||
"message_template": """
|
"message": "flash.logged_out.message",
|
||||||
You've been logged out.
|
|
||||||
""",
|
|
||||||
"category": "info",
|
"category": "info",
|
||||||
},
|
},
|
||||||
"login_next": {
|
"login_next": {
|
||||||
"title_template": translate("flash.login_required_title"),
|
"title": "flash.login_required_title",
|
||||||
"message_template": translate("flash.login_required_message"),
|
"message": "flash.login_required_message",
|
||||||
"category": "warning",
|
"category": "warning",
|
||||||
},
|
},
|
||||||
"new_application_member": {
|
"new_application_member": {
|
||||||
"title_template": """{{ "flash.new_application_member.title" | translate({ "user_name": user_name }) }}""",
|
"title": "flash.new_application_member.title",
|
||||||
"message_template": """
|
"message": "flash.new_application_member.message",
|
||||||
<p>{{ "flash.new_application_member.message" | translate({ "user_name": user_name }) }}</p>
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"new_portfolio_member": {
|
"new_portfolio_member": {
|
||||||
"title_template": translate("flash.success"),
|
"title": "flash.success",
|
||||||
"message_template": """
|
"message": "flash.new_portfolio_member",
|
||||||
<p>{{ "flash.new_portfolio_member" | translate({ "user_name": user_name }) }}</p>
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"portfolio_member_removed": {
|
"portfolio_member_removed": {
|
||||||
"title_template": translate("flash.deleted_member"),
|
"title": "flash.deleted_member",
|
||||||
"message_template": """
|
"message": "flash.delete_member_success",
|
||||||
{{ "flash.delete_member_success" | translate({ "member_name": member_name }) }}
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"primary_point_of_contact_changed": {
|
"primary_point_of_contact_changed": {
|
||||||
"title_template": translate("flash.new_ppoc_title"),
|
"title": "flash.new_ppoc_title",
|
||||||
"message_template": """{{ "flash.new_ppoc_message" | translate({ "ppoc_name": ppoc_name }) }}""",
|
"message": "flash.new_ppoc_message",
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"resend_portfolio_invitation": {
|
"resend_portfolio_invitation": {
|
||||||
"title_template": "Invitation resent",
|
"title": "flash.portfolio_invite.resent.title",
|
||||||
"message_template": """
|
"message": "flash.portfolio_invite.resent.message",
|
||||||
<p>Successfully sent a new invitation to {{ user_name }}.</p>
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"revoked_portfolio_access": {
|
"revoked_portfolio_access": {
|
||||||
"title_template": "Removed portfolio access",
|
"title": "flash.portfolio_member.revoked.title",
|
||||||
"message_template": """
|
"message": "flash.portfolio_member.revoked.message",
|
||||||
<p>Portfolio access successfully removed from {{ member_name }}.</p>
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"session_expired": {
|
"session_expired": {
|
||||||
"title_template": "Session Expired",
|
"title": "flash.session_expired.title",
|
||||||
"message_template": """
|
"message": "flash.session_expired.message",
|
||||||
Your session expired due to inactivity. Please log in again to continue.
|
|
||||||
""",
|
|
||||||
"category": "error",
|
"category": "error",
|
||||||
},
|
},
|
||||||
"task_order_draft": {
|
"task_order_draft": {
|
||||||
"title_template": translate("task_orders.form.draft_alert_title"),
|
"title": "task_orders.form.draft_alert_title",
|
||||||
"message_template": translate("task_orders.form.draft_alert_message"),
|
"message": "task_orders.form.draft_alert_message",
|
||||||
"category": "warning",
|
"category": "warning",
|
||||||
},
|
},
|
||||||
"task_order_number_error": {
|
"task_order_number_error": {
|
||||||
"title_template": "",
|
"title": None,
|
||||||
"message_template": """{{ 'flash.task_order_number_error.message' | translate({ 'to_number': to_number }) }}""",
|
"message": "flash.task_order_number_error.message",
|
||||||
"category": "error",
|
"category": "error",
|
||||||
},
|
},
|
||||||
"task_order_submitted": {
|
"task_order_submitted": {
|
||||||
"title_template": "Your Task Order has been uploaded successfully.",
|
"title": "flash.task_order.submitted.title",
|
||||||
"message_template": """
|
"message": "flash.task_order.submitted.message",
|
||||||
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>
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"updated_application_team_settings": {
|
"updated_application_team_settings": {
|
||||||
"title_template": translate("flash.success"),
|
"title": "flash.success",
|
||||||
"message_template": """
|
"message": "flash.updated_application_team_settings",
|
||||||
<p>{{ "flash.updated_application_team_settings" | translate({"application_name": application_name}) }}</p>
|
|
||||||
""",
|
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
"user_must_complete_profile": {
|
"user_must_complete_profile": {
|
||||||
"title_template": "You must complete your profile",
|
"title": "flash.user.complete_profile.title",
|
||||||
"message_template": "<p>Before continuing, you must complete your profile</p>",
|
"message": "flash.user.complete_profile.message",
|
||||||
"category": "info",
|
"category": "info",
|
||||||
},
|
},
|
||||||
"user_updated": {
|
"user_updated": {
|
||||||
"title_template": "User information updated.",
|
"title": "flash.user.updated.title",
|
||||||
"message_template": "",
|
"message": None,
|
||||||
"category": "success",
|
"category": "success",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -206,9 +173,11 @@ MESSAGES = {
|
|||||||
|
|
||||||
def formatted_flash(message_name, **message_args):
|
def formatted_flash(message_name, **message_args):
|
||||||
config = MESSAGES[message_name]
|
config = MESSAGES[message_name]
|
||||||
title = render_template_string(config["title_template"], **message_args)
|
|
||||||
message = render_template_string(config["message_template"], **message_args)
|
title = translate(config["title"], message_args) if config["title"] else None
|
||||||
actions = None
|
message = translate(config["message"], message_args) if config["message"] else None
|
||||||
if "actions" in config:
|
actions = (
|
||||||
actions = render_template_string(config["actions"], **message_args)
|
translate(config["actions"], message_args) if config.get("actions") else None
|
||||||
|
)
|
||||||
|
|
||||||
flash({"title": title, "message": message, "actions": actions}, config["category"])
|
flash({"title": title, "message": message, "actions": actions}, config["category"])
|
||||||
|
@ -2,16 +2,22 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import g, request, has_request_context
|
from flask import g, request, has_request_context, session
|
||||||
|
|
||||||
|
|
||||||
class RequestContextFilter(logging.Filter):
|
class RequestContextFilter(logging.Filter):
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
if has_request_context():
|
if has_request_context():
|
||||||
if getattr(g, "current_user", None):
|
if getattr(g, "current_user", None):
|
||||||
record.user_id = str(g.current_user.id)
|
|
||||||
record.dod_edipi = g.current_user.dod_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"):
|
if request.environ.get("HTTP_X_REQUEST_ID"):
|
||||||
record.request_id = 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")),
|
("request_id", lambda r: r.__dict__.get("request_id")),
|
||||||
("user_id", lambda r: r.__dict__.get("user_id")),
|
("user_id", lambda r: r.__dict__.get("user_id")),
|
||||||
("dod_edipi", lambda r: r.__dict__.get("dod_edipi")),
|
("dod_edipi", lambda r: r.__dict__.get("dod_edipi")),
|
||||||
|
("logged_in", lambda r: r.__dict__.get("logged_in")),
|
||||||
("severity", lambda r: r.levelname),
|
("severity", lambda r: r.levelname),
|
||||||
("tags", lambda r: r.__dict__.get("tags")),
|
("tags", lambda r: r.__dict__.get("tags")),
|
||||||
("audit_event", lambda r: r.__dict__.get("audit_event")),
|
("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:
|
for field, func in self._DEFAULT_RECORD_FIELDS:
|
||||||
result = func(record)
|
result = func(record)
|
||||||
if result:
|
if result is not None:
|
||||||
message_dict[field] = result
|
message_dict[field] = result
|
||||||
|
|
||||||
if record.args:
|
if record.args:
|
||||||
|
@ -3,6 +3,7 @@ ASSETS_URL
|
|||||||
AZURE_ACCOUNT_NAME
|
AZURE_ACCOUNT_NAME
|
||||||
AZURE_STORAGE_KEY
|
AZURE_STORAGE_KEY
|
||||||
AZURE_TO_BUCKET_NAME
|
AZURE_TO_BUCKET_NAME
|
||||||
|
AZURE_POLICY_LOCATION=policies
|
||||||
BLOB_STORAGE_URL=http://localhost:8000/
|
BLOB_STORAGE_URL=http://localhost:8000/
|
||||||
CAC_URL = http://localhost:8000/login-redirect
|
CAC_URL = http://localhost:8000/login-redirect
|
||||||
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
CA_CHAIN = ssl/server-certs/ca-chain.pem
|
||||||
@ -39,6 +40,7 @@ REDIS_USER
|
|||||||
SECRET_KEY = change_me_into_something_secret
|
SECRET_KEY = change_me_into_something_secret
|
||||||
SERVER_NAME
|
SERVER_NAME
|
||||||
SESSION_COOKIE_NAME=atat
|
SESSION_COOKIE_NAME=atat
|
||||||
|
SESSION_COOKIE_DOMAIN
|
||||||
SESSION_TYPE = redis
|
SESSION_TYPE = redis
|
||||||
SESSION_USE_SIGNER = True
|
SESSION_USE_SIGNER = True
|
||||||
SQLALCHEMY_ECHO = False
|
SQLALCHEMY_ECHO = False
|
||||||
|
@ -30,6 +30,7 @@ data:
|
|||||||
PGUSER: atat_master@atat-db
|
PGUSER: atat_master@atat-db
|
||||||
REDIS_HOST: atat.redis.cache.windows.net:6380
|
REDIS_HOST: atat.redis.cache.windows.net:6380
|
||||||
REDIS_TLS: "true"
|
REDIS_TLS: "true"
|
||||||
|
SESSION_COOKIE_DOMAIN: atat.code.mil
|
||||||
STATIC_URL: https://atat-cdn.azureedge.net/static/
|
STATIC_URL: https://atat-cdn.azureedge.net/static/
|
||||||
TZ: UTC
|
TZ: UTC
|
||||||
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
UWSGI_CONFIG_FULLPATH: /opt/atat/atst/uwsgi.ini
|
||||||
|
@ -45,7 +45,7 @@ data:
|
|||||||
include /etc/nginx/snippets/ssl.conf;
|
include /etc/nginx/snippets/ssl.conf;
|
||||||
|
|
||||||
location /login-redirect {
|
location /login-redirect {
|
||||||
return 301 https://auth-azure.atat.code.mil$request_uri;
|
return 301 https://${AUTH_DOMAIN}$request_uri;
|
||||||
}
|
}
|
||||||
location /login-dev {
|
location /login-dev {
|
||||||
try_files $uri @appbasicauth;
|
try_files $uri @appbasicauth;
|
||||||
@ -82,7 +82,7 @@ data:
|
|||||||
include /etc/nginx/snippets/ssl.conf;
|
include /etc/nginx/snippets/ssl.conf;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
return 301 https://azure.atat.code.mil$request_uri;
|
return 301 https://${MAIN_DOMAIN}$request_uri;
|
||||||
}
|
}
|
||||||
location /login-redirect {
|
location /login-redirect {
|
||||||
try_files $uri @app;
|
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/
|
- ../../azure/
|
||||||
resources:
|
resources:
|
||||||
- namespace.yml
|
- namespace.yml
|
||||||
|
- reset-cron-job.yml
|
||||||
patchesStrategicMerge:
|
patchesStrategicMerge:
|
||||||
- replica_count.yml
|
- replica_count.yml
|
||||||
- ports.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 { emitFieldChange } from '../lib/emitters'
|
||||||
|
import escape from '../lib/escape'
|
||||||
import optionsinput from './options_input'
|
import optionsinput from './options_input'
|
||||||
import textinput from './text_input'
|
import textinput from './text_input'
|
||||||
import clindollaramount from './clin_dollar_amount'
|
import clindollaramount from './clin_dollar_amount'
|
||||||
@ -99,7 +100,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
clinTitle: function() {
|
clinTitle: function() {
|
||||||
if (!!this.clinNumber) {
|
if (!!this.clinNumber) {
|
||||||
return `CLIN ${this.clinNumber}`
|
return escape(`CLIN ${this.clinNumber}`)
|
||||||
} else {
|
} else {
|
||||||
return `CLIN`
|
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
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
# script/cibuild: Run CI related checks and tests
|
# script/cibuild: Run CI related checks and tests
|
||||||
|
|
||||||
|
@ -26,28 +26,16 @@ fi
|
|||||||
|
|
||||||
## Main
|
## Main
|
||||||
if [ "${RUN_PYTHON_TESTS}" = "true" ]; then
|
if [ "${RUN_PYTHON_TESTS}" = "true" ]; then
|
||||||
python_test_status=0
|
|
||||||
set +e
|
|
||||||
|
|
||||||
output_divider "Lint Python files"
|
output_divider "Lint Python files"
|
||||||
run_python_lint "${PYTHON_FILES}"
|
run_python_lint "${PYTHON_FILES}"
|
||||||
((python_test_status+=$?))
|
|
||||||
output_divider "Perform static analysis on Python files"
|
output_divider "Perform static analysis on Python files"
|
||||||
run_python_static_analysis "${PYTHON_FILES}"
|
run_python_static_analysis "${PYTHON_FILES}"
|
||||||
((python_test_status+=$?))
|
|
||||||
output_divider "Perform type checking on Python files"
|
output_divider "Perform type checking on Python files"
|
||||||
run_python_typecheck
|
run_python_typecheck
|
||||||
((python_test_status+=$?))
|
|
||||||
output_divider "Run Python unit test suite"
|
output_divider "Run Python unit test suite"
|
||||||
run_python_unit_tests "${PYTHON_FILES}"
|
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
|
fi
|
||||||
|
|
||||||
if [ "${RUN_JS_TESTS}" = "true" ]; then
|
if [ "${RUN_JS_TESTS}" = "true" ]; then
|
||||||
|
@ -72,15 +72,18 @@ $CONTAINER_IMAGE \
|
|||||||
|
|
||||||
# Use curl to wait for application container to become available
|
# Use curl to wait for application container to become available
|
||||||
docker pull curlimages/curl:latest
|
docker pull curlimages/curl:latest
|
||||||
|
echo "Waiting for application container to become available"
|
||||||
docker run --network atat \
|
docker run --network atat \
|
||||||
curlimages/curl:latest \
|
curlimages/curl:latest \
|
||||||
curl --connect-timeout 3 \
|
curl \
|
||||||
|
--silent \
|
||||||
|
--connect-timeout 3 \
|
||||||
--max-time 5 \
|
--max-time 5 \
|
||||||
--retry $CONTAINER_TIMEOUT \
|
--retry $CONTAINER_TIMEOUT \
|
||||||
--retry-connrefused \
|
--retry-connrefused \
|
||||||
--retry-delay 1 \
|
--retry-delay 1 \
|
||||||
--retry-max-time $CONTAINER_TIMEOUT \
|
--retry-max-time $CONTAINER_TIMEOUT \
|
||||||
test-atat:8000
|
test-atat:8000 >/dev/null
|
||||||
|
|
||||||
# Run Ghost Inspector tests
|
# Run Ghost Inspector tests
|
||||||
docker pull ghostinspector/test-runner-standalone:latest
|
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 atst.utils import pick
|
||||||
|
|
||||||
from tests.factories import (
|
from tests.factories import (
|
||||||
random_service_branch,
|
random_defense_component,
|
||||||
TaskOrderFactory,
|
TaskOrderFactory,
|
||||||
CLINFactory,
|
CLINFactory,
|
||||||
AttachmentFactory,
|
AttachmentFactory,
|
||||||
@ -159,7 +159,7 @@ def get_users():
|
|||||||
|
|
||||||
def add_members_to_portfolio(portfolio):
|
def add_members_to_portfolio(portfolio):
|
||||||
for user_data in PORTFOLIO_USERS:
|
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 = {
|
profile = {
|
||||||
k: user_data[k] for k in user_data if k not in ["dod_id", "permission_sets"]
|
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(
|
portfolio = Portfolios.create(
|
||||||
user=portfolio_owner,
|
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)
|
add_task_orders_to_portfolio(portfolio)
|
||||||
@ -336,7 +336,7 @@ def seed_db():
|
|||||||
user=amanda,
|
user=amanda,
|
||||||
portfolio_attrs={
|
portfolio_attrs={
|
||||||
"name": "TIE Interceptor",
|
"name": "TIE Interceptor",
|
||||||
"defense_component": random_service_branch(),
|
"defense_component": random_defense_component(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
add_task_orders_to_portfolio(tie_interceptor)
|
add_task_orders_to_portfolio(tie_interceptor)
|
||||||
@ -347,7 +347,7 @@ def seed_db():
|
|||||||
user=amanda,
|
user=amanda,
|
||||||
portfolio_attrs={
|
portfolio_attrs={
|
||||||
"name": "TIE Fighter",
|
"name": "TIE Fighter",
|
||||||
"defense_component": random_service_branch(),
|
"defense_component": random_defense_component(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
add_task_orders_to_portfolio(tie_fighter)
|
add_task_orders_to_portfolio(tie_fighter)
|
||||||
@ -363,7 +363,7 @@ def seed_db():
|
|||||||
user=user,
|
user=user,
|
||||||
portfolio_attrs={
|
portfolio_attrs={
|
||||||
"name": ship,
|
"name": ship,
|
||||||
"defense_component": random_service_branch(),
|
"defense_component": random_defense_component(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
add_task_orders_to_portfolio(portfolio)
|
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/dod_login_notice.scss";
|
||||||
@import "components/sticky_cta.scss";
|
@import "components/sticky_cta.scss";
|
||||||
@import "components/error_page.scss";
|
@import "components/error_page.scss";
|
||||||
|
@import "components/member_form.scss";
|
||||||
|
|
||||||
@import "sections/login";
|
@import "sections/login";
|
||||||
@import "sections/home";
|
@import "sections/home";
|
||||||
|
@ -3,8 +3,7 @@
|
|||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
border-top: 1px solid $color-gray-lightest;
|
border-top: 1px solid $color-gray-lightest;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: $gap * 1.5;
|
padding: $gap * 1.5;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -15,19 +14,7 @@
|
|||||||
color: $color-gray-dark;
|
color: $color-gray-dark;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|
||||||
&__info {
|
&__login {
|
||||||
flex-grow: 1;
|
|
||||||
padding-left: 0.8rem;
|
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 Grid
|
||||||
.form-row {
|
.form-row {
|
||||||
margin: ($gap * 4) 0;
|
margin: ($gap * 4) 0;
|
||||||
&--separated {
|
|
||||||
|
&--bordered {
|
||||||
border-bottom: $color-gray-lighter 1px solid;
|
border-bottom: $color-gray-lighter 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,3 +203,7 @@
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-container__half {
|
||||||
|
max-width: 46rem;
|
||||||
|
}
|
||||||
|
@ -4,18 +4,4 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
box-shadow: $box-shadow;
|
box-shadow: $box-shadow;
|
||||||
margin-bottom: -$footer-height * 2.5;
|
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;
|
margin-left: 2 * $gap;
|
||||||
|
|
||||||
.line {
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 2px;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid $color-gray-lightest;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.portfolio-header {
|
.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 {
|
.links {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: $small-font-size;
|
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 {
|
@mixin subheading {
|
||||||
@ -138,6 +85,10 @@
|
|||||||
.portfolio-content {
|
.portfolio-content {
|
||||||
margin: (4 * $gap) $gap 0 $gap;
|
margin: (4 * $gap) $gap 0 $gap;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
a.add-new-button {
|
a.add-new-button {
|
||||||
display: inherit;
|
display: inherit;
|
||||||
margin-left: auto;
|
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 {
|
a.modal-link.icon-link {
|
||||||
float: right;
|
float: right;
|
||||||
|
|
||||||
@ -607,3 +520,28 @@
|
|||||||
margin-right: $gap * 3;
|
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-size: $small-font-size;
|
||||||
font-weight: $font-bold;
|
font-weight: $font-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--link {
|
||||||
|
font-weight: $font-bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,88 +12,44 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar__link {
|
&__link {
|
||||||
color: $color-white;
|
color: $color-white !important;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: $topbar-height;
|
height: $topbar-height;
|
||||||
padding: 0 ($gap * 2);
|
padding: 0 ($gap * 2);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
@include h5;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding-left: $gap;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&-label {
|
|
||||||
@include h5;
|
|
||||||
text-decoration: underline;
|
|
||||||
padding-left: $gap;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
margin-left: $gap;
|
|
||||||
|
|
||||||
@include icon-color($color-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--home {
|
|
||||||
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 {
|
&-icon {
|
||||||
display: flex;
|
margin-left: $gap;
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: flex-end;
|
|
||||||
|
|
||||||
.topbar__portfolio-menu {
|
@include icon-color($color-white);
|
||||||
margin-right: auto;
|
}
|
||||||
position: relative;
|
|
||||||
}
|
&--home {
|
||||||
|
padding-left: $gap / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-primary-darker;
|
||||||
|
color: $color-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.topbar--public {
|
&__context {
|
||||||
background-color: $color-primary;
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
.topbar__navigation {
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
align-items: stretch;
|
||||||
-ms-flex-pack: justify;
|
justify-content: flex-end;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
hr {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid $color-gray-light;
|
border-bottom: 1px solid $color-gray-light;
|
||||||
margin: ($gap * 3) ($site-margins * -4);
|
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-expanded-width: 25rem;
|
||||||
$sidenav-collapsed-width: 10rem;
|
$sidenav-collapsed-width: 10rem;
|
||||||
$max-panel-width: 80rem;
|
$max-panel-width: 80rem;
|
||||||
|
$home-pg-icon-width: 6rem;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* USWDS Variables
|
* USWDS Variables
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
.usa-button,
|
.usa-button,
|
||||||
a {
|
a {
|
||||||
margin: 0 0 0 $gap;
|
margin: 0 0 0 $gap;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
@include media($medium-screen) {
|
@include media($medium-screen) {
|
||||||
margin: 0 0 0 ($gap * 2);
|
margin: 0 0 0 ($gap * 2);
|
||||||
@ -46,7 +47,7 @@
|
|||||||
background: white;
|
background: white;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding-right: $gap * 4;
|
padding-right: $gap * 4;
|
||||||
border-top: 1px solid $color-gray-light;
|
border-top: 1px solid $color-gray-lighter;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
@ -94,4 +94,19 @@
|
|||||||
&--primary {
|
&--primary {
|
||||||
@include icon-color($color-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-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin-left: 3rem;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
left: -3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
@mixin sidenav__header {
|
|
||||||
padding: $gap ($gap * 2);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidenav-container {
|
.sidenav-container {
|
||||||
box-shadow: $box-shadow;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: $topbar-height + $usa-banner-height;
|
top: $topbar-height + $usa-banner-height;
|
||||||
@ -14,187 +8,11 @@
|
|||||||
@extend .sidenav-container;
|
@extend .sidenav-container;
|
||||||
width: $sidenav-collapsed-width;
|
width: $sidenav-collapsed-width;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__fixed {
|
.sidenav {
|
||||||
position: fixed;
|
width: $sidenav-expanded-width;
|
||||||
}
|
position: fixed;
|
||||||
|
|
||||||
.sidenav {
|
|
||||||
width: $sidenav-expanded-width;
|
|
||||||
|
|
||||||
@include media($large-screen) {
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
@include sidenav__header;
|
|
||||||
|
|
||||||
font-size: $h3-font-size;
|
|
||||||
text-transform: uppercase;
|
|
||||||
width: 50%;
|
|
||||||
color: $color-gray-dark;
|
|
||||||
opacity: 0.54;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__toggle {
|
|
||||||
@include sidenav__header;
|
|
||||||
|
|
||||||
font-size: $small-font-size;
|
|
||||||
line-height: 2.8rem;
|
|
||||||
float: right;
|
|
||||||
color: $color-blue-darker;
|
|
||||||
|
|
||||||
.toggle-arrows {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
&.sidenav__list--padded {
|
|
||||||
margin-top: 4 * $gap;
|
|
||||||
margin-bottom: $footer-height;
|
|
||||||
padding-bottom: $gap;
|
|
||||||
position: fixed;
|
|
||||||
overflow-y: scroll;
|
|
||||||
top: $topbar-height + $usa-banner-height + 4rem;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: $sidenav-expanded-width;
|
|
||||||
background-color: $color-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__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;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
display: block;
|
|
||||||
padding: $gap ($gap * 2);
|
|
||||||
color: $color-black;
|
|
||||||
text-decoration: underline;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
background-color: $color-aqua-lightest;
|
|
||||||
|
|
||||||
.sidenav__link-icon {
|
|
||||||
@include icon-style-active;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--minimized {
|
&--minimized {
|
||||||
@extend .sidenav;
|
@extend .sidenav;
|
||||||
@ -202,4 +20,110 @@
|
|||||||
width: $sidenav-collapsed-width;
|
width: $sidenav-collapsed-width;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include media($large-screen) {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
padding: $gap ($gap * 2);
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid $color-gray-lighter;
|
||||||
|
|
||||||
|
&--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 {
|
||||||
|
font-size: $small-font-size;
|
||||||
|
color: $color-blue;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: $gap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle-arrows {
|
||||||
|
vertical-align: middle;
|
||||||
|
@include icon-size(20);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
margin-top: 3 * $gap;
|
||||||
|
margin-bottom: $footer-height;
|
||||||
|
padding-bottom: ($gap * 2);
|
||||||
|
position: fixed;
|
||||||
|
overflow-y: scroll;
|
||||||
|
top: $topbar-height + $usa-banner-height + 4rem;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: $sidenav-expanded-width;
|
||||||
|
background-color: $color-white;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&--no-header {
|
||||||
|
top: $topbar-height + $usa-banner-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
color: $color-black-light !important;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
@include h4;
|
||||||
|
|
||||||
|
background-color: $color-aqua-lightest !important;
|
||||||
|
color: $color-primary-darker !important;
|
||||||
|
box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-primary !important;
|
||||||
|
background-color: $color-aqua-lightest;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,24 +21,8 @@ table.atat-table {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--align-center {
|
&--third {
|
||||||
text-align: center;
|
width: 33%;
|
||||||
}
|
|
||||||
|
|
||||||
&--shrink {
|
|
||||||
width: 1%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--expand {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--hide-small {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
@include media($medium-screen) {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,7 +37,6 @@ table.atat-table {
|
|||||||
padding: $gap * 2;
|
padding: $gap * 2;
|
||||||
border: 1px solid $color-gray-lighter;
|
border: 1px solid $color-gray-lighter;
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
@ -84,28 +67,6 @@ table.atat-table {
|
|||||||
|
|
||||||
@include panel-margin;
|
@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 {
|
table {
|
||||||
margin-bottom: 0;
|
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 {
|
.environment-roles {
|
||||||
padding: 0 ($gap * 3) ($gap * 3);
|
padding: 0 ($gap * 3) ($gap * 3);
|
||||||
|
|
||||||
|
@ -1,49 +1,25 @@
|
|||||||
.home {
|
.home {
|
||||||
|
margin: $gap * 3;
|
||||||
.sticky-cta {
|
.sticky-cta {
|
||||||
margin: -1.6rem -1.6rem 0 -1.6rem;
|
margin: -1.6rem -1.6rem 0 -1.6rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.about-cloud {
|
&__content {
|
||||||
margin: 4rem auto;
|
margin: 4rem;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
}
|
|
||||||
|
|
||||||
.your-project {
|
&--descriptions {
|
||||||
margin-top: 3rem;
|
.col {
|
||||||
padding: 3rem;
|
margin-left: $home-pg-icon-width;
|
||||||
background-color: $color-gray-lightest;
|
padding: ($gap * 2) ($gap * 4);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
h2 {
|
.icon--home-pg-badge {
|
||||||
margin-top: 0;
|
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 {
|
.reporting-expended-funding {
|
||||||
&__header {
|
&__header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -20,10 +20,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
.h2,
|
margin-bottom: $gap * 6;
|
||||||
p {
|
|
||||||
margin-bottom: $gap * 0.5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.col {
|
.col {
|
||||||
@ -39,22 +36,6 @@
|
|||||||
margin-top: $gap * 2;
|
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 {
|
.card {
|
||||||
padding: ($gap * 4) ($gap * 5) 0;
|
padding: ($gap * 4) ($gap * 5) 0;
|
||||||
|
|
||||||
@ -155,6 +136,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__confirmation {
|
||||||
|
margin-left: $gap * 8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-order__modal-cancel {
|
.task-order__modal-cancel {
|
||||||
@ -170,3 +155,16 @@
|
|||||||
padding-bottom: $gap * 2.5;
|
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_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) }}
|
{{ CheckboxInput(form.perms_del_env, classes="input__inline-fields", key=del_env, id=del_env, optional=True) }}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr class="full-width">
|
||||||
<div class="environment_roles environment-roles-new">
|
<div class="environment_roles environment-roles-new">
|
||||||
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
|
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
|
||||||
<p class='usa-input__help subtitle'>
|
<p class='usa-input__help subtitle'>
|
||||||
@ -118,7 +118,7 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro InfoFields(member_form) %}
|
{% 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.first_name, validation='requiredField', optional=False) }}
|
||||||
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
|
{{ TextInput(member_form.last_name, validation='requiredField', optional=False) }}
|
||||||
{{ TextInput(member_form.email, validation='email', 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/icon.html" import Icon %}
|
||||||
{% from "components/label.html" import Label %}
|
{% 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 %}
|
{% import "applications/fragments/member_form_fields.html" as member_fields %}
|
||||||
{% from "components/modal.html" import Modal %}
|
{% from "components/modal.html" import Modal %}
|
||||||
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
{% from "components/multi_step_modal_form.html" import MultiStepModalForm %}
|
||||||
{% from "components/save_button.html" import SaveButton %}
|
{% from "components/save_button.html" import SaveButton %}
|
||||||
{% from "components/toggle_list.html" import ToggleButton, ToggleSection %}
|
|
||||||
|
|
||||||
{% macro MemberManagementTemplate(
|
{% macro MemberManagementTemplate(
|
||||||
application,
|
application,
|
||||||
@ -40,7 +38,7 @@
|
|||||||
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
{% call Modal(modal_name, classes="form-content--app-mem") %}
|
||||||
<div class="modal__form--header">
|
<div class="modal__form--header">
|
||||||
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
|
||||||
<hr>
|
<hr class="full-width">
|
||||||
</div>
|
</div>
|
||||||
<base-form inline-template>
|
<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,) }}">
|
<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") %}
|
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
|
||||||
<div class="modal__form--header">
|
<div class="modal__form--header">
|
||||||
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
|
||||||
<hr>
|
<hr class="full-width">
|
||||||
</div>
|
</div>
|
||||||
<base-form inline-template :enable-save="true">
|
<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) }}">
|
<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=new_member_form,
|
||||||
form_action=url_for(action_new, application_id=application.id),
|
form_action=url_for(action_new, application_id=application.id),
|
||||||
steps=[
|
steps=[
|
||||||
member_steps.MemberStepOne(new_member_form),
|
member_form.BasicStep(
|
||||||
member_steps.MemberStepTwo(new_member_form, application)
|
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 %}
|
{% 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) }}
|
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="panel__break">
|
<hr>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-col form-col--two-thirds">
|
<div class="form-col form-col--two-thirds">
|
||||||
{{ TextInput(form.description, paragraph=True, optional=True) }}
|
{{ TextInput(form.description, paragraph=True, optional=True) }}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ 'portfolios.applications.new.step_2_description' | translate }}
|
{{ 'portfolios.applications.new.step_2_description' | translate }}
|
||||||
</p>
|
</p>
|
||||||
<hr class="panel__break">
|
<hr>
|
||||||
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
<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">
|
<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>
|
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ ('portfolios.applications.new.step_3_description' | translate) }}
|
{{ ('portfolios.applications.new.step_3_description' | translate) }}
|
||||||
</p>
|
</p>
|
||||||
<hr class="panel__break">
|
<hr>
|
||||||
|
|
||||||
{{ MemberManagementTemplate(
|
{{ MemberManagementTemplate(
|
||||||
application,
|
application,
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
|
|
||||||
{% block application_content %}
|
{% block application_content %}
|
||||||
|
|
||||||
|
{% if show_flash -%}
|
||||||
|
{% include "fragments/flash.html" %}
|
||||||
|
{%- endif %}
|
||||||
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
|
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
|
||||||
|
|
||||||
{% if user_can(permissions.EDIT_APPLICATION) %}
|
{% if user_can(permissions.EDIT_APPLICATION) %}
|
||||||
@ -59,59 +62,8 @@
|
|||||||
environments_obj,
|
environments_obj,
|
||||||
new_env_form) }}
|
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) %}
|
{% if user_can(permissions.VIEW_APPLICATION_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %}
|
||||||
|
<hr>
|
||||||
{% include "fragments/audit_events_log.html" %}
|
{% include "fragments/audit_events_log.html" %}
|
||||||
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
|
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -21,9 +21,7 @@
|
|||||||
{% include 'navigation/topbar.html' %}
|
{% include 'navigation/topbar.html' %}
|
||||||
|
|
||||||
<div class='global-layout'>
|
<div class='global-layout'>
|
||||||
{% if portfolios %}
|
{% include 'navigation/global_sidenav.html' %}
|
||||||
{% include 'navigation/global_sidenav.html' %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class='global-panel-container'>
|
<div class='global-panel-container'>
|
||||||
{% block sidenav %}{% endblock %}
|
{% block sidenav %}{% endblock %}
|
||||||
|
@ -9,11 +9,15 @@
|
|||||||
"text": "changes pending",
|
"text": "changes pending",
|
||||||
"color": "default",
|
"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 }}'>
|
<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>
|
</span>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endmacro %}
|
{%- 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) -%}
|
||||||
|
<li class="sidenav__item">
|
||||||
{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
|
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
|
||||||
<li>
|
<span class="sidenav__link-label">
|
||||||
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
|
{{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>
|
</span>
|
||||||
{% endif %}
|
</a>
|
||||||
</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>
|
</li>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% macro Tooltip(message,title='Help', classes="") %}
|
{% macro Tooltip(message,title='Help', classes="") %}
|
||||||
|
|
||||||
<button type="button" tabindex="0" class="icon-tooltip {{classes}}" v-tooltip.top="{content: '{{message}}', container: false}">
|
<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>
|
</button>
|
||||||
|
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
<footer class='app-footer'>
|
<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 %}
|
{% 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>
|
{{ "footer.login" | translate }} <local-datetime timestamp='{{ g.last_login }}' format='MMM D YYYY H:mm Z'></local-datetime>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -6,31 +6,26 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class='global-layout'>
|
<div class='global-layout'>
|
||||||
<div class='global-navigation sidenav'>
|
<div class='global-navigation'>
|
||||||
<ul>
|
<div class="sidenav-container">
|
||||||
|
<div class="sidenav">
|
||||||
{{ SidenavItem("JEDI Cloud Help",
|
<ul class="sidenav__list sidenav__list--no-header">
|
||||||
href = url_for("atst.helpdocs"),
|
{{ SidenavItem("JEDI Cloud Help",
|
||||||
active = not doc,
|
href = url_for("atst.helpdocs"),
|
||||||
icon='help'
|
active = not doc,
|
||||||
)}}
|
)}}
|
||||||
|
{% for doc_item in docs %}
|
||||||
{% for doc_item in docs %}
|
{% set active = doc and doc == doc_item %}
|
||||||
{% set active = doc and doc == doc_item %}
|
{{ SidenavItem(doc_item | title,
|
||||||
|
href = url_for("atst.helpdocs", doc=doc_item),
|
||||||
{{ SidenavItem(doc_item | title,
|
active = active,
|
||||||
href = url_for("atst.helpdocs", doc=doc_item),
|
)}}
|
||||||
active = active,
|
{% endfor %}
|
||||||
subnav = subnav or None
|
</ul>
|
||||||
)}}
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class='global-panel-container'>
|
<div class='global-panel-container'>
|
||||||
|
|
||||||
<div class='panel'>
|
<div class='panel'>
|
||||||
<div class='panel__heading panel__heading--divider'>
|
<div class='panel__heading panel__heading--divider'>
|
||||||
<h1>
|
<h1>
|
||||||
@ -42,16 +37,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='panel__content'>
|
<div class='panel__content'>
|
||||||
|
|
||||||
{% block doc_content %}
|
{% block doc_content %}
|
||||||
<p>Welcome to the JEDI Cloud help documentation.</p>
|
<p>Welcome to the JEDI Cloud help documentation.</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% from "components/sticky_cta.html" import StickyCTA %}
|
|
||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %}
|
{% from "components/sticky_cta.html" import StickyCTA %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -13,88 +12,56 @@
|
|||||||
{% set sticky_header = "home.get_started" | translate %}
|
{% set sticky_header = "home.get_started" | translate %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% call StickyCTA(sticky_header) %}
|
<div class="home__content">
|
||||||
<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">
|
|
||||||
{% include "fragments/flash.html" %}
|
{% include "fragments/flash.html" %}
|
||||||
|
|
||||||
<h1>{{ "home.head" | translate }}</h1>
|
<h1>{{ "home.head" | translate }}</h1>
|
||||||
|
<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>
|
||||||
|
|
||||||
{{ SemiCollapsibleText(first_half=("home.about_cloud.part1"|translate), second_half=("home.about_cloud.part2"|translate)) }}
|
<div class="home__content--descriptions">
|
||||||
|
<div class="row">
|
||||||
<div class="your-project">
|
<div class="col col--half col--pad">
|
||||||
<h2 class="h3">{{ "home.your_project" | translate }}</h2>
|
{{ Icon('funding', classes="icon--home-pg-badge") }}
|
||||||
<p>{{ "home.your_project_descrip" | translate }}</p>
|
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.funding" | translate }}</h4>
|
||||||
|
<p>
|
||||||
<hr>
|
{{ "home.funding_descrip" | translate }}
|
||||||
|
</p>
|
||||||
{% 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>
|
</div>
|
||||||
{% endmacro %}
|
<div class="col col--half col--pad">
|
||||||
|
{{ Icon('chart-pie', classes="icon--home-pg-badge") }}
|
||||||
<toggler inline-template v-bind:initial-selected-section="'funding'">
|
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.reports" | translate }}</h4>
|
||||||
<div>
|
<p>
|
||||||
<div class="portfolio-header">
|
{{ "home.reports_descrip" | translate }}
|
||||||
<div class="links row">
|
</p>
|
||||||
{{ 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 }}
|
|
||||||
</p>
|
|
||||||
{% endmacro %}
|
|
||||||
<div class="project-section-descriptions">
|
|
||||||
{{ Description('funding', default=True) }}
|
|
||||||
{{ Description('applications') }}
|
|
||||||
{{ Description('reports') }}
|
|
||||||
{{ Description('admin') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</toggler>
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
<img id='jedi-heirarchy' src="{{ url_for("static", filename="img/JEDIhierarchyDiagram.png")}}" alt="JEDI heirarchy diagram">
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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