Merge branch 'staging' of github-DDS:dod-ccpo/atst into gi-updates-wo-20191209

This commit is contained in:
Jay R. Newlin (PromptWorks) 2019-12-13 13:52:24 -05:00
commit 2a77dbe063
27 changed files with 504 additions and 286 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null
},
"generated_at": "2019-12-05T17:54:05Z",
"generated_at": "2019-12-06T21:22:07Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -161,7 +161,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false,
"is_verified": false,
"line_number": 31,
"line_number": 41,
"type": "Hex High Entropy String"
}
],

View File

@ -29,6 +29,7 @@ azure-mgmt-subscription = "*"
azure-graphrbac = "*"
msrestazure = "*"
azure-mgmt-authorization = "*"
azure-mgmt-managementgroups = "*"
[dev-packages]
bandit = "*"

177
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "6d2ab855267daac877ae7464de9dba5b62b7d89288992f87d8fc6ff0c0d2520f"
"sha256": "c2b19c436646705ea3bf4df8c35c2833083f048da37fc619e66f7236153607c5"
},
"pipfile-spec": 6,
"requires": {
@ -39,11 +39,11 @@
},
"apache-libcloud": {
"hashes": [
"sha256:201751f738109f25d58dcdfb5804e17216e0dc8f68b522e9e26ac16e0b9ff2ea",
"sha256:40215db1bd489d17dc1abfdb289d7f035313c7297b6a7462c79d8287cbbeae91"
"sha256:9bc5cd5c32151bb7a04a7c7de0be9b4a4b8271e348ac91dd79eaaeeae627115f",
"sha256:fcc165f2cc2db9a379c6d3a17b3beb9081bb64ba5c0bf7bbb58da864810092f0"
],
"index": "pypi",
"version": "==2.6.0"
"version": "==2.6.1"
},
"azure-common": {
"hashes": [
@ -68,6 +68,14 @@
"index": "pypi",
"version": "==0.60.0"
},
"azure-mgmt-managementgroups": {
"hashes": [
"sha256:3d5237947458dc94b4a392141174b1c1258d26611241ee104e9006d1d798f682",
"sha256:8194ee6274df865eccd1ed9d385ea625aeba9b8058b9e4fdf547f5207271a775"
],
"index": "pypi",
"version": "==0.2.0"
},
"azure-mgmt-subscription": {
"hashes": [
"sha256:504b4c42ba859070c3c50637ec07ca36aca600e613fcccaa398db22822fe21f1",
@ -117,10 +125,10 @@
},
"certifi": {
"hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
"version": "==2019.9.11"
"version": "==2019.11.28"
},
"cffi": {
"hashes": [
@ -248,10 +256,10 @@
},
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
],
"version": "==0.23"
"version": "==1.1.0"
},
"isodate": {
"hashes": [
@ -330,10 +338,10 @@
},
"more-itertools": {
"hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
],
"version": "==7.2.0"
"version": "==8.0.0"
},
"msrest": {
"hashes": [
@ -422,11 +430,11 @@
},
"pyopenssl": {
"hashes": [
"sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200",
"sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6"
"sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
"sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
],
"index": "pypi",
"version": "==19.0.0"
"version": "==19.1.0"
},
"python-dateutil": {
"hashes": [
@ -459,22 +467,20 @@
},
"pyyaml": {
"hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
],
"index": "pypi",
"version": "==5.1.2"
"version": "==5.2"
},
"redis": {
"hashes": [
@ -650,10 +656,10 @@
},
"certifi": {
"hashes": [
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
"version": "==2019.9.11"
"version": "==2019.11.28"
},
"chardet": {
"hashes": [
@ -785,10 +791,10 @@
},
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
"sha256:b044f07694ef14a6683b097ba56bd081dbc7cdc7c7fe46011e499dfecc082f21",
"sha256:e6ac600a142cf2db707b1998382cc7fc3b02befb7273876e01b8ad10b9652742"
],
"version": "==0.23"
"version": "==1.1.0"
},
"ipdb": {
"hashes": [
@ -799,11 +805,11 @@
},
"ipython": {
"hashes": [
"sha256:dfd303b270b7b5232b3d08bd30ec6fd685d8a58cabd54055e3d69d8f029f7280",
"sha256:ed7ebe1cba899c1c3ccad6f7f1c2d2369464cc77dba8eebc65e2043e19cda995"
"sha256:c66c7e27239855828a764b1e8fc72c24a6f4498a2637572094a78c5551fb9d51",
"sha256:f186b01b36609e0c5d0de27c7ef8e80c990c70478f8c880863004b3489a9030e"
],
"index": "pypi",
"version": "==7.9.0"
"version": "==7.10.1"
},
"ipython-genutils": {
"hashes": [
@ -908,30 +914,30 @@
},
"more-itertools": {
"hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
"sha256:53ff73f186307d9c8ef17a9600309154a6ae27f25579e80af4db8f047ba14bc2",
"sha256:a0ea684c39bc4315ba7aae406596ef191fd84f873d2d2751f84d64e81a7a2d45"
],
"version": "==7.2.0"
"version": "==8.0.0"
},
"mypy": {
"hashes": [
"sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f",
"sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00",
"sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae",
"sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d",
"sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9",
"sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391",
"sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f",
"sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9",
"sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990",
"sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7",
"sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb",
"sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453",
"sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b",
"sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb"
"sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768",
"sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646",
"sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c",
"sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939",
"sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279",
"sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2",
"sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2",
"sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640",
"sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7",
"sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c",
"sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17",
"sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78",
"sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931",
"sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a"
],
"index": "pypi",
"version": "==0.740"
"version": "==0.750"
},
"mypy-extensions": {
"hashes": [
@ -961,10 +967,10 @@
},
"pbr": {
"hashes": [
"sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8",
"sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"
"sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
"sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
],
"version": "==5.4.3"
"version": "==5.4.4"
},
"pexpect": {
"hashes": [
@ -983,18 +989,17 @@
},
"pluggy": {
"hashes": [
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"version": "==0.13.0"
"version": "==0.13.1"
},
"prompt-toolkit": {
"hashes": [
"sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4",
"sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31",
"sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"
"sha256:0278d2f51b5ceba6ea8da39f76d15684e84c996b325475f6e5720edc584326a7",
"sha256:63daee79aa8366c8f1c637f1a4876b890da5fc92a19ebd2f7080ebacb901e990"
],
"version": "==2.0.10"
"version": "==3.0.2"
},
"ptyprocess": {
"hashes": [
@ -1012,10 +1017,10 @@
},
"pygments": {
"hashes": [
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
],
"version": "==2.4.2"
"version": "==2.5.2"
},
"pylint": {
"hashes": [
@ -1058,11 +1063,11 @@
},
"pytest-mock": {
"hashes": [
"sha256:b3514caac35fe3f05555923eabd9546abce11571cc2ddf7d8615959d04f2c89e",
"sha256:ea502c3891599c26243a3a847ccf0b1d20556678c528f86c98e3cd6d40c5cf11"
"sha256:96a0cebc66e09930be2a15b03333d90b59584d3fb011924f81c14b50ee0afbba",
"sha256:e5381be2608e49547f5e47633c5f81241ebf6206d17ce516a7a18d5a917e3859"
],
"index": "pypi",
"version": "==1.11.2"
"version": "==1.12.1"
},
"pytest-watch": {
"hashes": [
@ -1080,22 +1085,20 @@
},
"pyyaml": {
"hashes": [
"sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9",
"sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4",
"sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8",
"sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696",
"sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34",
"sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9",
"sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73",
"sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299",
"sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b",
"sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae",
"sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681",
"sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41",
"sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
],
"index": "pypi",
"version": "==5.1.2"
"version": "==5.2"
},
"regex": {
"hashes": [

View File

@ -3,6 +3,7 @@ import re
from uuid import uuid4
from atst.models.user import User
from atst.models.application import Application
from atst.models.environment import Environment
from atst.models.environment_role import EnvironmentRole
@ -399,13 +400,14 @@ REMOTE_ROOT_ROLE_DEF_ID = "/providers/Microsoft.Authorization/roleDefinitions/00
class AzureSDKProvider(object):
def __init__(self):
from azure.mgmt import subscription, authorization
from azure.mgmt import subscription, authorization, managementgroups
import azure.graphrbac as graphrbac
import azure.common.credentials as credentials
from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD
self.subscription = subscription
self.authorization = authorization
self.managementgroups = managementgroups
self.graphrbac = graphrbac
self.credentials = credentials
# may change to a JEDI cloud
@ -428,42 +430,23 @@ class AzureCloudProvider(CloudProviderInterface):
def create_environment(
self, auth_credentials: Dict, user: User, environment: Environment
):
# since this operation would only occur within a tenant, should we source the tenant
# via lookup from environment once we've created the portfolio csp data schema
# something like this:
# environment_tenant = environment.application.portfolio.csp_data.get('tenant_id', None)
# though we'd probably source the whole credentials for these calls from the portfolio csp
# data, as it would have to be where we store the creds for the at-at user within the portfolio tenant
# credentials = self._get_credential_obj(environment.application.portfolio.csp_data.get_creds())
credentials = self._get_credential_obj(self._root_creds)
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
management_group_id = "?" # management group id chained from environment
parent_id = "?" # from environment.application
billing_profile_id = "?" # something chained from environment?
sku_id = AZURE_SKU_ID
# we want to set AT-AT as an owner here
# we could potentially associate subscriptions with "management groups" per DOD component
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
display_name,
billing_profile_id,
sku_id,
# owner=<AdPrincipal: for AT-AT user>
management_group = self._create_management_group(
credentials, management_group_id, display_name, parent_id,
)
# These 2 seem like something that might be worthwhile to allow tiebacks to
# TOs filed for the environment
billing_account_name = "?"
invoice_section_name = "?"
# We may also want to create billing sections in the enrollment account
sub_creation_operation = sub_client.subscription_factory.create_subscription(
billing_account_name, invoice_section_name, body
)
# the resulting object from this process is a link to the new subscription
# not a subscription model, so we'll have to unpack the ID
new_sub = sub_creation_operation.result()
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
if subscription_id:
return subscription_id
else:
# troublesome error, subscription should exist at this point
# but we just don't have a valid ID
pass
return management_group
def create_atat_admin_user(
self, auth_credentials: Dict, csp_environment_id: str
@ -502,6 +485,83 @@ class AzureCloudProvider(CloudProviderInterface):
"role_name": role_assignment_id,
}
def _create_application(self, auth_credentials: Dict, application: Application):
management_group_name = str(uuid4()) # can be anything, not just uuid
display_name = application.name # Does this need to be unique?
credentials = self._get_credential_obj(auth_credentials)
parent_id = "?" # application.portfolio.csp_details.management_group_id
return self._create_management_group(
credentials, management_group_name, display_name, parent_id,
)
def _create_management_group(
self, credentials, management_group_id, display_name, parent_id=None,
):
mgmgt_group_client = self.sdk.managementgroups.ManagementGroupsAPI(credentials)
create_parent_grp_info = self.sdk.managementgroups.models.CreateParentGroupInfo(
id=parent_id
)
create_mgmt_grp_details = self.sdk.managementgroups.models.CreateManagementGroupDetails(
parent=create_parent_grp_info
)
mgmt_grp_create = self.sdk.managementgroups.models.CreateManagementGroupRequest(
name=management_group_id,
display_name=display_name,
details=create_mgmt_grp_details,
)
create_request = mgmgt_group_client.management_groups.create_or_update(
management_group_id, mgmt_grp_create
)
# result is a synchronous wait, might need to do a poll instead to handle first mgmt group create
# since we were told it could take 10+ minutes to complete, unless this handles that polling internally
return create_request.result()
def _create_subscription(
self,
credentials,
display_name,
billing_profile_id,
sku_id,
management_group_id,
billing_account_name,
invoice_section_name,
):
sub_client = self.sdk.subscription.SubscriptionClient(credentials)
display_name = f"{environment.application.name}_{environment.name}_{environment.id}" # proposed format
billing_profile_id = "?" # where do we source this?
sku_id = AZURE_SKU_ID
# These 2 seem like something that might be worthwhile to allow tiebacks to
# TOs filed for the environment
billing_account_name = "?" # from TO?
invoice_section_name = "?" # from TO?
body = self.sdk.subscription.models.ModernSubscriptionCreationParameters(
display_name=display_name,
billing_profile_id=billing_profile_id,
sku_id=sku_id,
management_group_id=management_group_id,
)
# We may also want to create billing sections in the enrollment account
sub_creation_operation = sub_client.subscription_factory.create_subscription(
billing_account_name, invoice_section_name, body
)
# the resulting object from this process is a link to the new subscription
# not a subscription model, so we'll have to unpack the ID
new_sub = sub_creation_operation.result()
subscription_id = self._extract_subscription_id(new_sub.subscription_link)
if subscription_id:
return subscription_id
else:
# troublesome error, subscription should exist at this point
# but we just don't have a valid ID
pass
def _get_management_service_principal(self):
# we really should be using graph.microsoft.com, but i'm getting
# "expired token" errors for that

View File

@ -64,10 +64,12 @@ class TaskOrders(BaseDomainClass):
db.session.commit()
@classmethod
def sort(cls, task_orders: [TaskOrder]) -> [TaskOrder]:
# Sorts a list of task orders on two keys: status (primary) and time_created (secondary)
by_time_created = sorted(task_orders, key=lambda to: to.time_created)
by_status = sorted(by_time_created, key=lambda to: SORT_ORDERING.get(to.status))
def sort_by_status(cls, task_orders):
by_status = {status.value: [] for status in SORT_ORDERING}
for task_order in task_orders:
by_status[task_order.display_status].append(task_order)
return by_status
@classmethod

View File

@ -1,5 +1,5 @@
from datetime import timedelta
from enum import Enum
from decimal import Decimal
from sqlalchemy import Column, DateTime, ForeignKey, String
from sqlalchemy.ext.hybrid import hybrid_property
@ -17,15 +17,16 @@ class Status(Enum):
ACTIVE = "Active"
UPCOMING = "Upcoming"
EXPIRED = "Expired"
UNSIGNED = "Not signed"
UNSIGNED = "Unsigned"
SORT_ORDERING = {
status: order
for (order, status) in enumerate(
[Status.DRAFT, Status.ACTIVE, Status.UPCOMING, Status.EXPIRED, Status.UNSIGNED]
)
}
SORT_ORDERING = [
Status.ACTIVE,
Status.DRAFT,
Status.UPCOMING,
Status.EXPIRED,
Status.UNSIGNED,
]
class TaskOrder(Base, mixins.TimestampsMixin):
@ -131,12 +132,11 @@ class TaskOrder(Base, mixins.TimestampsMixin):
@property
def start_date(self):
return min((c.start_date for c in self.clins), default=self.time_created.date())
return min((c.start_date for c in self.clins), default=None)
@property
def end_date(self):
default_end_date = self.start_date + timedelta(days=1)
return max((c.end_date for c in self.clins), default=default_end_date)
return max((c.end_date for c in self.clins), default=None)
@property
def days_to_expiration(self):
@ -170,6 +170,11 @@ class TaskOrder(Base, mixins.TimestampsMixin):
# Faked for display purposes
return 50
@property
def invoiced_funds(self):
# TODO: implement this using reporting data from the CSP
return self.total_obligated_funds * Decimal(0.75)
@property
def display_status(self):
return self.status.value

View File

@ -20,6 +20,7 @@ from atst.domain.permission_sets import PermissionSets
from atst.utils.flash import formatted_flash as flash
from atst.utils.localization import translate
from atst.jobs import send_mail
from atst.routes.errors import log_error
def get_environments_obj_for_app(application):
@ -234,7 +235,8 @@ def handle_update_member(application_id, application_role_id, form_data):
flash("application_member_updated", user_name=app_role.user_name)
except GeneralCSPException:
except GeneralCSPException as exc:
log_error(exc)
flash(
"application_member_update_error", user_name=app_role.user_name,
)

View File

@ -6,7 +6,6 @@ from atst.domain.portfolios import Portfolios
from atst.domain.task_orders import TaskOrders
from atst.forms.task_order import SignatureForm
from atst.models import Permissions
from atst.models.task_order import Status as TaskOrderStatus
@task_orders_bp.route("/task_orders/<task_order_id>/review")
@ -28,14 +27,9 @@ def review_task_order(task_order_id):
@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding")
def portfolio_funding(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
task_orders = TaskOrders.sort(portfolio.task_orders)
label_colors = {
TaskOrderStatus.DRAFT: "warning",
TaskOrderStatus.ACTIVE: "success",
TaskOrderStatus.UPCOMING: "info",
TaskOrderStatus.EXPIRED: "error",
TaskOrderStatus.UNSIGNED: "purple",
}
task_orders = TaskOrders.sort_by_status(portfolio.task_orders)
to_count = len(portfolio.task_orders)
# TODO: Get expended amount from the CSP
return render_template(
"task_orders/index.html", task_orders=task_orders, label_colors=label_colors
"task_orders/index.html", task_orders=task_orders, to_count=to_count
)

View File

@ -13,10 +13,10 @@ data:
CDN_ORIGIN: https://azure.atat.code.mil
CELERY_DEFAULT_QUEUE: celery-master
CSP: azure
DEBUG: 0
DEBUG: "0"
FLASK_ENV: master
LOG_JSON: "true"
MAIL_PORT: 587
MAIL_PORT: "587"
MAIL_SENDER: postmaster@atat.code.mil
MAIL_SERVER: smtp.mailgun.org
MAIL_TLS: "true"
@ -24,7 +24,7 @@ data:
PGAPPNAME: atst
PGDATABASE: staging
PGHOST: atat-db.postgres.database.azure.com
PGPORT: 5432
PGPORT: "5432"
PGSSLMODE: verify-full
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
PGUSER: atat_master@atat-db

View File

@ -9,9 +9,9 @@ data:
AZURE_TO_BUCKET_NAME: task-order-pdfs
CAC_URL: https://auth-staging.atat.code.mil/login-redirect
CELERY_DEFAULT_QUEUE: celery-master
DEBUG: 0
DEBUG: "0"
DISABLE_CRL_CHECK: "true"
MAIL_PORT: 587
MAIL_PORT: "587"
MAIL_SENDER: postmaster@atat.code.mil
MAIL_SERVER: smtp.mailgun.org
MAIL_TLS: "true"
@ -19,7 +19,7 @@ data:
PGAPPNAME: atst
PGDATABASE: staging
PGHOST: atat-db.postgres.database.azure.com
PGPORT: 5432
PGPORT: "5432"
PGSSLMODE: verify-full
PGSSLROOTCERT: /opt/atat/atst/ssl/pgsslrootcert.crt
PGUSER: atat_master@atat-db

View File

@ -11,4 +11,10 @@ export default {
default: false,
},
},
methods: {
collapse: function() {
this.isVisible = false
},
},
}

View File

@ -0,0 +1,16 @@
import Accordion from './accordion'
export default {
name: 'accordion-list',
components: {
Accordion,
},
methods: {
handleClick: function(e) {
e.preventDefault()
this.$children.forEach(el => el.collapse())
},
},
}

View File

@ -7,6 +7,8 @@ import Vue from 'vue/dist/vue'
import VTooltip from 'v-tooltip'
import stickybits from 'stickybits'
import Accordion from './components/accordion'
import AccordionList from './components/accordion_list'
import dodlogin from './components/dodlogin'
import optionsinput from './components/options_input'
import multicheckboxinput from './components/multi_checkbox_input'
@ -29,7 +31,6 @@ import SemiCollapsibleText from './components/semi_collapsible_text'
import ToForm from './components/forms/to_form'
import ClinFields from './components/clin_fields'
import PopDateRange from './components/pop_date_range'
import Accordion from './components/accordion'
import ToggleMenu from './components/toggle_menu'
Vue.config.productionTip = false
@ -42,6 +43,7 @@ const app = new Vue({
el: '#app-root',
components: {
Accordion,
AccordionList,
dodlogin,
toggler,
optionsinput,

View File

@ -17,6 +17,7 @@ export default {
methods: {
toggle: function(e) {
e.preventDefault()
e.stopPropagation()
this.isVisible = !this.isVisible
},
},

View File

@ -46,5 +46,20 @@
margin: 0;
}
}
&--empty {
font-weight: $font-bold;
color: $color-gray-dark;
padding: $gap * 8;
text-align: center;
}
}
&-list {
max-width: $max-panel-width;
&__collapse {
cursor: pointer;
}
}
}

View File

@ -127,21 +127,6 @@
width: 100%;
}
.label {
&--pending,
&--started {
background-color: $color-gold;
}
&--active {
background-color: $color-green;
}
&--expired {
background-color: $color-red;
}
}
.task-order-document-link {
&__icon {
padding-top: 0.5rem;

View File

@ -1,4 +1,5 @@
{% from "components/accordion.html" import Accordion %}
{% from "components/accordion_list.html" import AccordionList %}
{% from "components/empty_state.html" import EmptyState %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% from "components/icon.html" import Icon %}
@ -32,7 +33,7 @@
) }}
{% else %}
<div class="usa-accordion">
{% call AccordionList() %}
{% for application in portfolio.applications|sort(attribute='name') %}
{% set section_name = "application-{}".format(application.id) %}
{% set title = "Environments ({})".format(application.environments|length) %}
@ -76,7 +77,7 @@
{% endcall %}
</div>
{% endfor %}
</div>
{% endcall %}
{% endif %}
</div>

View File

@ -0,0 +1,11 @@
{% macro AccordionList() %}
<accordion-list inline-template>
<div class="accordion-list usa-accordion">
<div class="action-group">
<a v-on:click="handleClick($event)" class="accordion-list__collapse">Collapse All</a>
</div>
<!-- caller iterates over accordion vue components or Accordion jinja macros -->
{{ caller() }}
</div>
</accordion-list>
{% endmacro %}

View File

@ -1,4 +1,5 @@
{% from "components/accordion.html" import Accordion %}
{% from "components/accordion_list.html" import AccordionList %}
{% from "components/empty_state.html" import EmptyState %}
{% from "components/icon.html" import Icon %}
{% from "components/sticky_cta.html" import StickyCTA %}
@ -13,38 +14,52 @@
{% macro TaskOrderList(task_orders, status) %}
{% set status = "All Task Orders" %}
<div class="accordion usa-accordion">
{% call Accordion(title=status, id=status, heading_tag="h4") %}
{% for task_order in task_orders %}
<div class="accordion__content--list-item">
<h4><a href="{{ url_for('task_orders.review_task_order', task_order_id=task_order.id) }}">Task Order #{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}</a></h4>
<div class="row">
<div class="col col--grow">
<h5>
Current Period of Performance
</h5>
<p>
{{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }}
-
{{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }}
</p>
</div>
<div class="col col--grow">
<h5>Total Value</h5>
<p>{{ task_order.total_contract_amount | dollars }}</p>
</div>
<div class="col col--grow">
<h5>Total Obligated</h5>
<p>{{ task_order.total_obligated_funds | dollars }}</p>
</div>
<div class="col col--grow">
<h5>Total Expended</h5>
<p>$0</p>
</div>
<div class="accordion">
{% call Accordion(title=("task_orders.status_list_title"|translate({'status': status})), id=status, heading_tag="h4") %}
{% if task_orders|length > 0 %}
{% for task_order in task_orders %}
{% set to_number %}
{% if task_order.number != "" %}
Task Order #{{ task_order.number }}
{% else %}
New Task Order
{% endif %}
{% endset %}
<div class="accordion__content--list-item">
<h4><a href="{{ url_for('task_orders.review_task_order', task_order_id=task_order.id) }}">{{ to_number }} {{ Icon("caret_right", classes="icon--tiny icon--primary" ) }}</a></h4>
{% if status != 'Expired' -%}
<div class="row">
<div class="col col--grow">
<h5>
Current Period of Performance
</h5>
<p>
{{ task_order.start_date | formattedDate(formatter="%b %d, %Y") }}
-
{{ task_order.end_date | formattedDate(formatter="%b %d, %Y") }}
</p>
</div>
<div class="col col--grow">
<h5>Total Value</h5>
<p>{{ task_order.total_contract_amount | dollars }}</p>
</div>
<div class="col col--grow">
<h5>Total Obligated</h5>
<p>{{ task_order.total_obligated_funds | dollars }}</p>
</div>
<div class="col col--grow">
<h5>Total Expended</h5>
<p>{{ task_order.invoiced_funds | dollars }}</p>
</div>
</div>
{%- endif %}
</div>
{% endfor %}
{% else %}
<div class="accordion__content--empty">
{{ "task_orders.status_empty_state" | translate({ 'status': status }) }}
</div>
{% endfor %}
{% endif %}
{% endcall %}
</div>
{% endmacro %}
@ -62,8 +77,12 @@
<div class="portfolio-funding">
{% if task_orders %}
{{ TaskOrderList(task_orders) }}
{% if to_count > 0 %}
{% call AccordionList() %}
{% for status, to_list in task_orders.items() %}
{{ TaskOrderList(to_list, status) }}
{% endfor %}
{% endcall %}
{% else %}
{{ EmptyState(
header="task_orders.empty_state.header"|translate,

View File

@ -1,27 +1,81 @@
import pytest
from unittest.mock import Mock
from uuid import uuid4
from atst.domain.csp.cloud import AzureCloudProvider
from tests.mock_azure import mock_azure, AUTH_CREDENTIALS
from tests.factories import EnvironmentFactory
from tests.factories import EnvironmentFactory, ApplicationFactory
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
# TODO: Directly test create subscription, provide all args √
# TODO: Test create environment (create management group with parent)
# TODO: Test create application (create manageemnt group with parent)
# Create reusable mock for mocking the management group calls for multiple services
#
def test_create_subscription_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create()
subscription_id = str(uuid4())
credentials = mock_azure._get_credential_obj(AUTH_CREDENTIALS)
display_name = "Test Subscription"
billing_profile_id = str(uuid4())
sku_id = str(uuid4())
management_group_id = (
environment.cloud_id # environment.csp_details.management_group_id?
)
billing_account_name = (
"?" # environment.application.portfilio.csp_details.billing_account.name?
)
invoice_section_name = "?" # environment.name? or something specific to billing?
mock_azure.sdk.subscription.SubscriptionClient.return_value.subscription_factory.create_subscription.return_value.result.return_value.subscription_link = (
f"subscriptions/{subscription_id}"
)
result = mock_azure._create_subscription(
credentials,
display_name,
billing_profile_id,
sku_id,
management_group_id,
billing_account_name,
invoice_section_name,
)
assert result == subscription_id
def mock_management_group_create(mock_azure, spec_dict):
mock_azure.sdk.managementgroups.ManagementGroupsAPI.return_value.management_groups.create_or_update.return_value.result.return_value = Mock(
**spec_dict
)
def test_create_environment_succeeds(mock_azure: AzureCloudProvider):
environment = EnvironmentFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"})
result = mock_azure.create_environment(
AUTH_CREDENTIALS, environment.creator, environment
)
assert result == subscription_id
assert result.id == "Test Id"
def test_create_application_succeeds(mock_azure: AzureCloudProvider):
application = ApplicationFactory.create()
mock_management_group_create(mock_azure, {"id": "Test Id"})
result = mock_azure._create_application(AUTH_CREDENTIALS, application)
assert result.id == "Test Id"
def test_create_atat_admin_user_succeeds(mock_azure: AzureCloudProvider):

View File

@ -3,78 +3,11 @@ from datetime import date, timedelta
from decimal import Decimal
from atst.domain.task_orders import TaskOrders
from atst.models import Attachment, TaskOrder
from atst.models import Attachment
from atst.models.task_order import TaskOrder, SORT_ORDERING, Status
from tests.factories import TaskOrderFactory, CLINFactory, PortfolioFactory
def test_task_order_sorting():
"""
Task orders should be listed first by status, and then by time_created.
"""
today = date.today()
yesterday = today - timedelta(days=1)
future = today + timedelta(days=100)
task_orders = [
# Draft
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
# Active
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
# Upcoming
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
# Expired
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
# Unsigned
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
]
assert TaskOrders.sort(task_orders) == task_orders
def test_create_adds_clins():
portfolio = PortfolioFactory.create()
clins = [
@ -177,3 +110,47 @@ def test_delete_task_order_with_clins(session):
assert not session.query(
session.query(TaskOrder).filter_by(id=task_order.id).exists()
).scalar()
def test_task_order_sort_by_status():
today = date.today()
yesterday = today - timedelta(days=1)
future = today + timedelta(days=100)
initial_to_list = [
# Draft
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
TaskOrderFactory.create(pdf=None),
# Active
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=future)],
),
# Upcoming
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=future, end_date=future)],
),
# Expired
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
TaskOrderFactory.create(
signed_at=yesterday,
clins=[CLINFactory.create(start_date=yesterday, end_date=yesterday)],
),
# Unsigned
TaskOrderFactory.create(
clins=[CLINFactory.create(start_date=today, end_date=today)]
),
]
sorted_by_status = TaskOrders.sort_by_status(initial_to_list)
assert len(sorted_by_status["Draft"]) == 3
assert len(sorted_by_status["Active"]) == 1
assert len(sorted_by_status["Upcoming"]) == 1
assert len(sorted_by_status["Expired"]) == 2
assert len(sorted_by_status["Unsigned"]) == 1
assert list(sorted_by_status.keys()) == [status.value for status in SORT_ORDERING]

View File

@ -10,9 +10,9 @@ AZURE_CONFIG = {
}
AUTH_CREDENTIALS = {
"CLIENT_ID": AZURE_CONFIG["AZURE_CLIENT_ID"],
"SECRET_KEY": AZURE_CONFIG["AZURE_SECRET_KEY"],
"TENANT_ID": AZURE_CONFIG["AZURE_TENANT_ID"],
"client_id": AZURE_CONFIG["AZURE_CLIENT_ID"],
"secret_key": AZURE_CONFIG["AZURE_SECRET_KEY"],
"tenant_id": AZURE_CONFIG["AZURE_TENANT_ID"],
}
@ -28,6 +28,12 @@ def mock_authorization():
return Mock(spec=authorization)
def mock_managementgroups():
from azure.mgmt import managementgroups
return Mock(spec=managementgroups)
def mock_graphrbac():
import azure.graphrbac as graphrbac
@ -46,6 +52,7 @@ class MockAzureSDK(object):
self.subscription = mock_subscription()
self.authorization = mock_authorization()
self.managementgroups = mock_managementgroups()
self.graphrbac = mock_graphrbac()
self.credentials = mock_credentials()
# may change to a JEDI cloud

View File

@ -12,6 +12,7 @@ from atst.domain.application_roles import ApplicationRoles
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.invitations import ApplicationInvitations
from atst.domain.common import Paginator
from atst.domain.csp.cloud import GeneralCSPException
from atst.domain.permission_sets import PermissionSets
from atst.models.application_role import Status as ApplicationRoleStatus
from atst.models.environment_role import CSPRole, EnvironmentRole
@ -748,3 +749,41 @@ def test_handle_update_member(set_g):
assert len(application.roles) == 1
assert len(app_role.environment_roles) == 1
assert app_role.environment_roles[0].environment == env
def test_handle_update_member_with_error(set_g, monkeypatch, mock_logger):
exception = "An error occurred."
def _raise_csp_exception(*args, **kwargs):
raise GeneralCSPException(exception)
monkeypatch.setattr(
"atst.domain.environments.Environments.update_env_role", _raise_csp_exception
)
user = UserFactory.create()
application = ApplicationFactory.create(
environments=[{"name": "Naboo"}, {"name": "Endor"}]
)
(env, env_1) = application.environments
app_role = ApplicationRoleFactory(application=application)
set_g("current_user", application.portfolio.owner)
set_g("portfolio", application.portfolio)
set_g("application", application)
form_data = ImmutableMultiDict(
{
"environment_roles-0-environment_id": env.id,
"environment_roles-0-role": "Basic Access",
"environment_roles-0-environment_name": env.name,
"environment_roles-1-environment_id": env_1.id,
"environment_roles-1-role": NO_ACCESS,
"environment_roles-1-environment_name": env_1.name,
"perms_env_mgmt": True,
"perms_team_mgmt": True,
"perms_del_env": True,
}
)
handle_update_member(application.id, app_role.id, form_data)
assert mock_logger.messages[-1] == exception

View File

@ -29,8 +29,10 @@ def task_order():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
attachment = Attachment(filename="sample_attachment", object_name="sample")
task_order = TaskOrderFactory.create(portfolio=portfolio)
CLINFactory.create(task_order=task_order)
return TaskOrderFactory.create(portfolio=portfolio)
return task_order
def test_review_task_order_not_draft(client, user_session, task_order):

View File

@ -19,6 +19,16 @@ def build_pdf_form_data(filename="sample.pdf", object_name=None):
def task_order():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
task_order = TaskOrderFactory.create(portfolio=portfolio)
CLINFactory.create(task_order=task_order)
return task_order
@pytest.fixture
def incomplete_to():
user = UserFactory.create()
portfolio = PortfolioFactory.create(owner=user)
return TaskOrderFactory.create(portfolio=portfolio)
@ -234,7 +244,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
},
]
TaskOrders.create_clins(task_order.id, clin_list)
assert len(task_order.clins) == 2
assert len(task_order.clins) == 3
user_session(task_order.portfolio.owner)
form_data = {
@ -267,11 +277,11 @@ def test_task_orders_form_step_four_review(client, user_session, completed_task_
def test_task_orders_form_step_four_review_incomplete_to(
client, user_session, task_order
client, user_session, incomplete_to
):
user_session(task_order.portfolio.owner)
user_session(incomplete_to.portfolio.owner)
response = client.get(
url_for("task_orders.form_step_four_review", task_order_id=task_order.id)
url_for("task_orders.form_step_four_review", task_order_id=incomplete_to.id)
)
assert response.status_code == 404
@ -290,12 +300,13 @@ def test_task_orders_form_step_five_confirm_signature(
def test_task_orders_form_step_five_confirm_signature_incomplete_to(
client, user_session, task_order
client, user_session, incomplete_to
):
user_session(task_order.portfolio.owner)
user_session(incomplete_to.portfolio.owner)
response = client.get(
url_for(
"task_orders.form_step_five_confirm_signature", task_order_id=task_order.id
"task_orders.form_step_five_confirm_signature",
task_order_id=incomplete_to.id,
)
)
assert response.status_code == 404

View File

@ -40,6 +40,9 @@ class FakeLogger:
def error(self, msg, *args, **kwargs):
self._log("error", msg, *args, **kwargs)
def exception(self, msg, *args, **kwargs):
self._log("exception", msg, *args, **kwargs)
def _log(self, _lvl, msg, *args, **kwargs):
self.messages.append(msg)
if "extra" in kwargs:

View File

@ -529,6 +529,8 @@ task_orders:
team_title: Your team
sign:
digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature.
status_empty_state: 'This Portfolio has no {status} Task Orders.'
status_list_title: '{status} Task Orders'
JEDICLINType:
JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS'
JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS'