Merge branch 'master' into ui/reports-spend-table
This commit is contained in:
commit
08783e60ce
1
.gitignore
vendored
1
.gitignore
vendored
@ -34,3 +34,4 @@ config/dev.ini
|
|||||||
# CRLs
|
# CRLs
|
||||||
/crl
|
/crl
|
||||||
/crl-tmp
|
/crl-tmp
|
||||||
|
*.bk
|
||||||
|
1
Pipfile
1
Pipfile
@ -29,6 +29,7 @@ black = "*"
|
|||||||
pytest-watch = "*"
|
pytest-watch = "*"
|
||||||
factory-boy = "*"
|
factory-boy = "*"
|
||||||
pytest-flask = "*"
|
pytest-flask = "*"
|
||||||
|
pytest-env = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.6"
|
python_version = "3.6"
|
||||||
|
139
Pipfile.lock
generated
139
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "5fc8273838354406366b401529a6f512a73ac6a8ecea6699afa4ab7b4996bf13"
|
"sha256": "41ad134816dae388385cfb15105e0eca436b25791ec4fbf67a2b36c4ae8056bd"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -33,10 +33,10 @@
|
|||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
|
"sha256:4c1d68a1408dd090d2f3a869aa94c3947cc1d967821d1ed303208c9f41f0f2f4",
|
||||||
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
|
"sha256:b6e8b28b2b7e771a41ecdd12d4d43262ecab52adebbafa42c77d6b57fb6ad3a4"
|
||||||
],
|
],
|
||||||
"version": "==2018.4.16"
|
"version": "==2018.8.13"
|
||||||
},
|
},
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -91,27 +91,27 @@
|
|||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:21af753934f2f6d1a10fe8f4c0a64315af209ef6adeaee63ca349797d747d687",
|
"sha256:02602e1672b62e803e08617ec286041cc453e8d43f093a5f4162095506bc0beb",
|
||||||
"sha256:27bb401a20a838d6d0ea380f08c6ead3ccd8c9d8a0232dc9adcc0e4994576a66",
|
"sha256:10b48e848e1edb93c1d3b797c83c72b4c387ab0eb4330aaa26da8049a6cbede0",
|
||||||
"sha256:29720c4253263cff9aea64585adbbe85013ba647f6e98367efff9db2d7193ded",
|
"sha256:17db09db9d7c5de130023657be42689d1a5f60502a14f6f745f6f65a6b8195c0",
|
||||||
"sha256:2a35b7570d8f247889784010aac8b384fd2e4a47b33e15c4a60b45a7c1944120",
|
"sha256:227da3a896df1106b1a69b1e319dce218fa04395e8cc78be7e31ca94c21254bc",
|
||||||
"sha256:42c531a6a354407f42ee07fda5c2c0dc822cf6d52744949c182f2b295fbd4183",
|
"sha256:2cbaa03ac677db6c821dac3f4cdfd1461a32d0615847eedbb0df54bb7802e1f7",
|
||||||
"sha256:5eb86f03f9c4f0ac2336ac5431271072ddf7ecc76b338e26366732cfac58aa19",
|
"sha256:31db8febfc768e4b4bd826750a70c79c99ea423f4697d1dab764eb9f9f849519",
|
||||||
"sha256:67f7f57eae8dede577f3f7775957f5bec93edd6bdb6ce597bb5b28e1bdf3d4fb",
|
"sha256:4a510d268e55e2e067715d728e4ca6cd26a8e9f1f3d174faf88e6f2cb6b6c395",
|
||||||
"sha256:6ec84edcbc966ae460560a51a90046503ff0b5b66157a9efc61515c68059f6c8",
|
"sha256:6a88d9004310a198c474d8a822ee96a6dd6c01efe66facdf17cb692512ae5bc0",
|
||||||
"sha256:7ba834564daef87557e7fcd35c3c3183a4147b0b3a57314e53317360b9b201b3",
|
"sha256:76936ec70a9b72eb8c58314c38c55a0336a2b36de0c7ee8fb874a4547cadbd39",
|
||||||
"sha256:7d7f084cbe1fdb82be5a0545062b59b1ad3637bc5a48612ac2eb428ff31b31ea",
|
"sha256:7e3b4aecc4040928efa8a7cdaf074e868af32c58ffc9bb77e7bf2c1a16783286",
|
||||||
"sha256:82409f5150e529d699e5c33fa8fd85e965104db03bc564f5f4b6a9199e591f7c",
|
"sha256:8168bcb08403ef144ff1fb880d416f49e2728101d02aaadfe9645883222c0aa5",
|
||||||
"sha256:87d092a7c2a44e5f7414ab02fb4145723ebba411425e1a99773531dd4c0e9b8d",
|
"sha256:8229ceb79a1792823d87779959184a1bf95768e9248c93ae9f97c7a2f60376a1",
|
||||||
"sha256:8c56ef989342e42b9fcaba7c74b446f0cc9bed546dd00034fa7ad66fc00307ef",
|
"sha256:8a19e9f2fe69f6a44a5c156968d9fc8df56d09798d0c6a34ccc373bb186cee86",
|
||||||
"sha256:9449f5d4d7c516a6118fa9210c4a00f34384cb1d2028672100ee0c6cce49d7f6",
|
"sha256:8d10113ca826a4c29d5b85b2c4e045ffa8bad74fb525ee0eceb1d38d4c70dfd6",
|
||||||
"sha256:bc2301170986ad82d9349a91eb8884e0e191209c45f5541b16aa7c0cfb135978",
|
"sha256:be495b8ec5a939a7605274b6e59fbc35e76f5ad814ae010eb679529671c9e119",
|
||||||
"sha256:c132bab45d4bd0fff1d3fe294d92b0a6eb8404e93337b3127bdec9f21de117e6",
|
"sha256:dc2d3f3b1548f4d11786616cf0f4415e25b0fbecb8a1d2cd8c07568f13fdde38",
|
||||||
"sha256:c3d945b7b577f07a477700f618f46cbc287af3a9222cd73035c6ef527ef2c363",
|
"sha256:e4aecdd9d5a3d06c337894c9a6e2961898d3f64fe54ca920a72234a3de0f9cb3",
|
||||||
"sha256:cee18beb4c807b5c0b178f4fa2fae03cef9d51821a358c6890f8b23465b7e5d2",
|
"sha256:e79ab4485b99eacb2166f3212218dd858258f374855e1568f728462b0e6ee0d9",
|
||||||
"sha256:d01dfc5c2b3495184f683574e03c70022674ca9a7be88589c5aba130d835ea90"
|
"sha256:f995d3667301e1754c57b04e0bae6f0fa9d710697a9f8d6712e8cca02550910f"
|
||||||
],
|
],
|
||||||
"version": "==2.3"
|
"version": "==2.3.1"
|
||||||
},
|
},
|
||||||
"flask": {
|
"flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -271,7 +271,7 @@
|
|||||||
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
|
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
|
||||||
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
|
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
|
||||||
],
|
],
|
||||||
"markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'",
|
"markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*'",
|
||||||
"version": "==2018.5"
|
"version": "==2018.5"
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
@ -299,10 +299,10 @@
|
|||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8"
|
"sha256:ef6569ad403520ee13e180e1bfd6ed71a0254192a934ec1dbd3dbf48f4aa9524"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.2.10"
|
"version": "==1.2.11"
|
||||||
},
|
},
|
||||||
"unipath": {
|
"unipath": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -317,7 +317,7 @@
|
|||||||
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
|
||||||
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.6' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version < '4'",
|
"markers": "python_version >= '2.6' and python_version != '3.2.*' and python_version != '3.0.*' and python_version < '4' and python_version != '3.1.*' and python_version != '3.3.*'",
|
||||||
"version": "==1.23"
|
"version": "==1.23"
|
||||||
},
|
},
|
||||||
"webassets": {
|
"webassets": {
|
||||||
@ -350,14 +350,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.4.3"
|
"version": "==1.4.3"
|
||||||
},
|
},
|
||||||
"appnope": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
|
|
||||||
"sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
|
|
||||||
],
|
|
||||||
"markers": "sys_platform == 'darwin'",
|
|
||||||
"version": "==0.1.0"
|
|
||||||
},
|
|
||||||
"argh": {
|
"argh": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3",
|
"sha256:a9b3aaa1904eeb78e32394cd46c6f37ac0fb4af6dc488daa58971bdc7d7fcaf3",
|
||||||
@ -367,10 +359,10 @@
|
|||||||
},
|
},
|
||||||
"astroid": {
|
"astroid": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a48b57ede295c3188ef5c84273bc2a8eadc46e4cbb001eae0d49fb5d1fabbb19",
|
"sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be",
|
||||||
"sha256:d066cdeec5faeb51a4be5010da612680653d844b57afd86a5c8315f2f801b4cc"
|
"sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d"
|
||||||
],
|
],
|
||||||
"version": "==2.0.2"
|
"version": "==2.0.4"
|
||||||
},
|
},
|
||||||
"atomicwrites": {
|
"atomicwrites": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -395,11 +387,11 @@
|
|||||||
},
|
},
|
||||||
"bandit": {
|
"bandit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:cb977045497f83ec3a02616973ab845c829cdab8144ce2e757fe031104a9abd4",
|
"sha256:45bf1b361004e861e5b423b36ff5c700d21442753c841013c87f14a4639b1d74",
|
||||||
"sha256:de4cc19d6ba32d6f542c6a1ddadb4404571347d83ef1ed1e7afb7d0b38e0c25b"
|
"sha256:a3aa04802194ec1fd290849e02b915824f9c3234623d7dcea6a33b1605ddb0ac"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.4.0"
|
"version": "==1.5.0"
|
||||||
},
|
},
|
||||||
"black": {
|
"black": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -446,11 +438,11 @@
|
|||||||
},
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867",
|
"sha256:ea7cfd3aeb1544732d08bd9cfba40c5b78e3a91e17b1a0698ab81bfc5554c628",
|
||||||
"sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d"
|
"sha256:f6d67f04abfb2b4bea7afc7fa6c18cf4c523a67956e455668be9ae42bccc21ad"
|
||||||
],
|
],
|
||||||
"markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*'",
|
"markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'",
|
||||||
"version": "==0.8.17"
|
"version": "==0.9.0"
|
||||||
},
|
},
|
||||||
"flask": {
|
"flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -502,7 +494,7 @@
|
|||||||
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
|
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
|
||||||
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
|
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
|
||||||
],
|
],
|
||||||
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
|
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
|
||||||
"version": "==4.3.4"
|
"version": "==4.3.4"
|
||||||
},
|
},
|
||||||
"itsdangerous": {
|
"itsdangerous": {
|
||||||
@ -620,7 +612,7 @@
|
|||||||
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
|
||||||
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
|
||||||
],
|
],
|
||||||
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
|
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
|
||||||
"version": "==0.7.1"
|
"version": "==0.7.1"
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
@ -643,7 +635,7 @@
|
|||||||
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
|
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
|
||||||
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
|
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
|
||||||
],
|
],
|
||||||
"markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'",
|
"markers": "python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*'",
|
||||||
"version": "==1.5.4"
|
"version": "==1.5.4"
|
||||||
},
|
},
|
||||||
"pygments": {
|
"pygments": {
|
||||||
@ -663,11 +655,18 @@
|
|||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:86a8dbf407e437351cef4dba46736e9c5a6e3c3ac71b2e942209748e76ff2086",
|
"sha256:3459a123ad5532852d36f6f4501dfe1acf4af1dd9541834a164666aa40395b02",
|
||||||
"sha256:e74466e97ac14582a8188ff4c53e6cc3810315f342f6096899332ae864c1d432"
|
"sha256:96bfd45dbe863b447a3054145cd78a9d7f31475d2bce6111b133c0cc4f305118"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.7.1"
|
"version": "==3.7.2"
|
||||||
|
},
|
||||||
|
"pytest-env": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.6.2"
|
||||||
},
|
},
|
||||||
"pytest-flask": {
|
"pytest-flask": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -748,42 +747,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==4.3.2"
|
"version": "==4.3.2"
|
||||||
},
|
},
|
||||||
"typed-ast": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
|
|
||||||
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
|
|
||||||
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
|
|
||||||
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
|
|
||||||
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
|
|
||||||
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
|
|
||||||
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
|
|
||||||
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
|
|
||||||
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
|
|
||||||
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
|
|
||||||
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
|
|
||||||
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
|
|
||||||
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
|
|
||||||
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
|
|
||||||
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
|
|
||||||
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
|
|
||||||
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
|
|
||||||
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
|
|
||||||
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
|
|
||||||
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
|
|
||||||
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
|
|
||||||
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
|
|
||||||
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
|
|
||||||
],
|
|
||||||
"version": "==1.1.0"
|
|
||||||
},
|
|
||||||
"typing": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf",
|
|
||||||
"sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8",
|
|
||||||
"sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"
|
|
||||||
],
|
|
||||||
"version": "==3.6.4"
|
|
||||||
},
|
|
||||||
"watchdog": {
|
"watchdog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162"
|
"sha256:7e65882adb7746039b6f3876ee174952f8eaaa34491ba34333ddf1fe35de4162"
|
||||||
|
45
README.md
45
README.md
@ -8,15 +8,36 @@ This is the user-facing web application for ATAT.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Requirements
|
### System Requirements
|
||||||
See the [scriptz](https://github.com/dod-ccpo/scriptz) repository for the shared
|
ATST uses the [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
||||||
requirements and guidelines for all ATAT applications.
|
pattern for setting up and running the project. The scripts are located in the
|
||||||
|
`scripts` directory and use script fragments in the
|
||||||
|
[scriptz](https://github.com/dod-ccpo/scriptz) repository that are shared across
|
||||||
|
ATAT repositories.
|
||||||
|
|
||||||
ATST requires a postgres instance (>= 9.6) for persistence. Have postgres installed
|
Before running the setup scripts, a couple of dependencies need to be installed
|
||||||
and running on the default port of 5432.
|
locally:
|
||||||
|
|
||||||
ATST also requires a redis instance for session management. Have redis installed and
|
* `python` == 3.6
|
||||||
running on the default port of 6379.
|
Python version 3.6 must be installed on your machine before installing `pipenv`.
|
||||||
|
You can download Python 3.6 [from python.org](https://www.python.org/downloads/)
|
||||||
|
or use your preferred system package manager.
|
||||||
|
|
||||||
|
* `pipenv`
|
||||||
|
ATST requires `pipenv` to be installed for python dependency management. `pipenv`
|
||||||
|
will create the virtual environment that the app requires. [See
|
||||||
|
`pipenv`'s documentation for instructions on installing `pipenv](
|
||||||
|
https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv).
|
||||||
|
|
||||||
|
* `postgres` >= 9.6
|
||||||
|
ATST requires a PostgreSQL instance (>= 9.6) for persistence. Have PostgresSQL installed
|
||||||
|
and running on the default port of 5432. You can verify that PostgresSQL is running
|
||||||
|
by executing `psql` and ensuring that a connection is successfully made.
|
||||||
|
|
||||||
|
* `redis`
|
||||||
|
ATST also requires a Redis instance for session management. Have Redis installed and
|
||||||
|
running on the default port of 6379. You can ensure that Redis is running by
|
||||||
|
executing `redis-cli` with no options and ensuring a connection is succesfully made.
|
||||||
|
|
||||||
### Cloning
|
### Cloning
|
||||||
This project contains git submodules. Here is an example clone command that will
|
This project contains git submodules. Here is an example clone command that will
|
||||||
@ -62,6 +83,10 @@ To watch for changes to any js/css assets:
|
|||||||
|
|
||||||
yarn watch
|
yarn watch
|
||||||
|
|
||||||
|
After running `script/dev_server`, the application is available at
|
||||||
|
[`http://localhost:8000`](http://localhost:8000).
|
||||||
|
|
||||||
|
|
||||||
### Users
|
### Users
|
||||||
|
|
||||||
There are currently six mock users for development:
|
There are currently six mock users for development:
|
||||||
@ -73,7 +98,11 @@ There are currently six mock users for development:
|
|||||||
- Dominick
|
- Dominick
|
||||||
- Erica
|
- Erica
|
||||||
|
|
||||||
To log in as one of them, navigate to `/login-dev?username=<lowercase name>`. For example `/login-dev?username=amanda`.
|
To log in as one of them, navigate to `/login-dev?username=<lowercase name>`.
|
||||||
|
For example `/login-dev?username=amanda`.
|
||||||
|
|
||||||
|
In development mode, there is a `DEV Login` button available on the home page
|
||||||
|
that will automatically log you in as Amanda.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
37
alembic/versions/4be312655ceb_add_workspaces_table.py
Normal file
37
alembic/versions/4be312655ceb_add_workspaces_table.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""add workspaces table
|
||||||
|
|
||||||
|
Revision ID: 4be312655ceb
|
||||||
|
Revises: 05d6272bdb43
|
||||||
|
Create Date: 2018-08-16 09:25:19.888549
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4be312655ceb'
|
||||||
|
down_revision = '05d6272bdb43'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('workspaces',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||||
|
sa.Column('request_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('task_order_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['request_id'], ['requests.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['task_order_id'], ['task_order.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('workspaces')
|
||||||
|
# ### end Alembic commands ###
|
30
alembic/versions/a2b499a1dd62_workspace_timestamps.py
Normal file
30
alembic/versions/a2b499a1dd62_workspace_timestamps.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""workspace timestamps
|
||||||
|
|
||||||
|
Revision ID: a2b499a1dd62
|
||||||
|
Revises: f549c7cee17c
|
||||||
|
Create Date: 2018-08-17 10:43:13.165829
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a2b499a1dd62'
|
||||||
|
down_revision = 'f549c7cee17c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('workspaces', sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False))
|
||||||
|
op.add_column('workspaces', sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('workspaces', 'time_updated')
|
||||||
|
op.drop_column('workspaces', 'time_created')
|
||||||
|
# ### end Alembic commands ###
|
47
alembic/versions/f064247f2988_projects_and_environments.py
Normal file
47
alembic/versions/f064247f2988_projects_and_environments.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""projects and environments
|
||||||
|
|
||||||
|
Revision ID: f064247f2988
|
||||||
|
Revises: a2b499a1dd62
|
||||||
|
Create Date: 2018-08-17 11:30:53.684954
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f064247f2988'
|
||||||
|
down_revision = 'a2b499a1dd62'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('projects',
|
||||||
|
sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(), nullable=False),
|
||||||
|
sa.Column('workspace_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('environments',
|
||||||
|
sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('environments')
|
||||||
|
op.drop_table('projects')
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
|||||||
|
"""add workspace_role workspace_id fk
|
||||||
|
|
||||||
|
Revision ID: f36f130622b9
|
||||||
|
Revises: f064247f2988
|
||||||
|
Create Date: 2018-08-20 10:36:23.920881
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f36f130622b9'
|
||||||
|
down_revision = 'f064247f2988'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_foreign_key('workspace_role_workspace_id_fk', 'workspace_role', 'workspaces', ['workspace_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('workspace_role_workspace_id_fk', 'workspace_role', type_='foreignkey')
|
||||||
|
# ### end Alembic commands ###
|
@ -0,0 +1,30 @@
|
|||||||
|
"""remove workspaces task order association
|
||||||
|
|
||||||
|
Revision ID: f549c7cee17c
|
||||||
|
Revises: 4be312655ceb
|
||||||
|
Create Date: 2018-08-16 16:42:48.581510
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f549c7cee17c'
|
||||||
|
down_revision = '4be312655ceb'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('workspaces_task_order_id_fkey', 'workspaces', type_='foreignkey')
|
||||||
|
op.drop_column('workspaces', 'task_order_id')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('workspaces', sa.Column('task_order_id', sa.INTEGER(), autoincrement=False))
|
||||||
|
op.create_foreign_key('workspaces_task_order_id_fkey', 'workspaces', 'task_order', ['task_order_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
@ -18,6 +18,7 @@ from atst.routes.dev import bp as dev_routes
|
|||||||
from atst.routes.errors import make_error_pages
|
from atst.routes.errors import make_error_pages
|
||||||
from atst.domain.authnid.crl import CRLCache
|
from atst.domain.authnid.crl import CRLCache
|
||||||
from atst.domain.auth import apply_authentication
|
from atst.domain.auth import apply_authentication
|
||||||
|
from atst.eda_client import MockEDAClient
|
||||||
|
|
||||||
|
|
||||||
ENV = os.getenv("FLASK_ENV", "dev")
|
ENV = os.getenv("FLASK_ENV", "dev")
|
||||||
@ -41,6 +42,7 @@ def make_app(config):
|
|||||||
make_flask_callbacks(app)
|
make_flask_callbacks(app)
|
||||||
make_crl_validator(app)
|
make_crl_validator(app)
|
||||||
register_filters(app)
|
register_filters(app)
|
||||||
|
make_eda_client(app)
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
@ -62,11 +64,6 @@ def make_app(config):
|
|||||||
def make_flask_callbacks(app):
|
def make_flask_callbacks(app):
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def _set_globals():
|
def _set_globals():
|
||||||
g.navigationContext = (
|
|
||||||
"workspace"
|
|
||||||
if re.match("\/workspaces\/[A-Za-z0-9]*", request.path)
|
|
||||||
else "global"
|
|
||||||
)
|
|
||||||
g.dev = os.getenv("FLASK_ENV", "dev") == "dev"
|
g.dev = os.getenv("FLASK_ENV", "dev") == "dev"
|
||||||
g.matchesPath = lambda href: re.match("^" + href, request.path)
|
g.matchesPath = lambda href: re.match("^" + href, request.path)
|
||||||
g.modal = request.args.get("modal", None)
|
g.modal = request.args.get("modal", None)
|
||||||
@ -139,3 +136,5 @@ def make_crl_validator(app):
|
|||||||
crl_locations.append(filename.absolute())
|
crl_locations.append(filename.absolute())
|
||||||
app.crl_cache = CRLCache(app.config["CA_CHAIN"], crl_locations, logger=app.logger)
|
app.crl_cache = CRLCache(app.config["CA_CHAIN"], crl_locations, logger=app.logger)
|
||||||
|
|
||||||
|
def make_eda_client(app):
|
||||||
|
app.eda_client = MockEDAClient()
|
||||||
|
12
atst/domain/authz.py
Normal file
12
atst/domain/authz.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from atst.domain.workspace_users import WorkspaceUsers
|
||||||
|
|
||||||
|
|
||||||
|
class Authorization(object):
|
||||||
|
@classmethod
|
||||||
|
def has_workspace_permission(cls, user, workspace, permission):
|
||||||
|
workspace_user = WorkspaceUsers.get(workspace.id, user.id)
|
||||||
|
return permission in workspace_user.permissions()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_in_workspace(cls, user, workspace):
|
||||||
|
return user in workspace.users
|
12
atst/domain/environments.py
Normal file
12
atst/domain/environments.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from atst.database import db
|
||||||
|
from atst.models.environment import Environment
|
||||||
|
|
||||||
|
|
||||||
|
class Environments(object):
|
||||||
|
@classmethod
|
||||||
|
def create(cls, project, name):
|
||||||
|
environment = Environment(project=project, name=name)
|
||||||
|
db.session.add(environment)
|
||||||
|
db.session.commit()
|
||||||
|
return environment
|
||||||
|
|
13
atst/domain/projects.py
Normal file
13
atst/domain/projects.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from atst.database import db
|
||||||
|
from atst.models.project import Project
|
||||||
|
|
||||||
|
|
||||||
|
class Projects(object):
|
||||||
|
@classmethod
|
||||||
|
def create(cls, workspace, name, description):
|
||||||
|
project = Project(workspace=workspace, name=name, description=description)
|
||||||
|
|
||||||
|
db.session.add(project)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return project
|
@ -6,6 +6,7 @@ from sqlalchemy.orm.attributes import flag_modified
|
|||||||
|
|
||||||
from atst.models.request import Request
|
from atst.models.request import Request
|
||||||
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
|
from atst.models.request_status_event import RequestStatusEvent, RequestStatus
|
||||||
|
from atst.domain.workspaces import Workspaces
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
|
|
||||||
from .exceptions import NotFoundError
|
from .exceptions import NotFoundError
|
||||||
@ -31,6 +32,7 @@ def deep_merge(source, destination: dict):
|
|||||||
|
|
||||||
class Requests(object):
|
class Requests(object):
|
||||||
AUTO_APPROVE_THRESHOLD = 1000000
|
AUTO_APPROVE_THRESHOLD = 1000000
|
||||||
|
ANNUAL_SPEND_THRESHOLD = 1000000
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, creator, body):
|
def create(cls, creator, body):
|
||||||
@ -114,6 +116,18 @@ class Requests(object):
|
|||||||
db.session.add(request)
|
db.session.add(request)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
return request
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def approve_and_create_workspace(cls, request):
|
||||||
|
approved_request = Requests.set_status(request, RequestStatus.APPROVED)
|
||||||
|
workspace = Workspaces.create(approved_request)
|
||||||
|
|
||||||
|
db.session.add(approved_request)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return workspace
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_status(cls, request: Request, status: RequestStatus):
|
def set_status(cls, request: Request, status: RequestStatus):
|
||||||
status_event = RequestStatusEvent(new_status=status)
|
status_event = RequestStatusEvent(new_status=status)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
|
from flask import current_app as app
|
||||||
|
|
||||||
from atst.database import db
|
from atst.database import db
|
||||||
from atst.models.task_order import TaskOrder
|
from atst.models.task_order import TaskOrder
|
||||||
@ -8,12 +9,36 @@ from .exceptions import NotFoundError
|
|||||||
class TaskOrders(object):
|
class TaskOrders(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(self, order_number):
|
def get(cls, order_number):
|
||||||
try:
|
try:
|
||||||
task_order = (
|
task_order = (
|
||||||
db.session.query(TaskOrder).filter_by(number=order_number).one()
|
db.session.query(TaskOrder).filter_by(number=order_number).one()
|
||||||
)
|
)
|
||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
raise NotFoundError("task_order")
|
if TaskOrders._client():
|
||||||
|
task_order = TaskOrders._get_from_eda(order_number)
|
||||||
|
else:
|
||||||
|
raise NotFoundError("task_order")
|
||||||
|
|
||||||
return task_order
|
return task_order
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_from_eda(cls, order_number):
|
||||||
|
to_data = TaskOrders._client().get_contract(order_number, status="y")
|
||||||
|
if to_data:
|
||||||
|
return TaskOrders.create(to_data["contract_no"])
|
||||||
|
else:
|
||||||
|
raise NotFoundError("task_order")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, order_number):
|
||||||
|
task_order = TaskOrder(number=order_number)
|
||||||
|
|
||||||
|
db.session.add(task_order)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return task_order
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _client(cls):
|
||||||
|
return app.eda_client
|
||||||
|
@ -21,7 +21,8 @@ class WorkspaceUsers(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
workspace_role = (
|
workspace_role = (
|
||||||
WorkspaceRole.query.join(User)
|
db.session.query(WorkspaceRole)
|
||||||
|
.join(User)
|
||||||
.filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id)
|
.filter(User.id == user_id, WorkspaceRole.workspace_id == workspace_id)
|
||||||
.one()
|
.one()
|
||||||
)
|
)
|
||||||
@ -30,6 +31,29 @@ class WorkspaceUsers(object):
|
|||||||
|
|
||||||
return WorkspaceUser(user, workspace_role)
|
return WorkspaceUser(user, workspace_role)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add(cls, user, workspace_id, role_name):
|
||||||
|
role = Roles.get(role_name)
|
||||||
|
try:
|
||||||
|
existing_workspace_role = (
|
||||||
|
db.session.query(WorkspaceRole)
|
||||||
|
.filter(
|
||||||
|
WorkspaceRole.user == user,
|
||||||
|
WorkspaceRole.workspace_id == workspace_id,
|
||||||
|
)
|
||||||
|
.one()
|
||||||
|
)
|
||||||
|
new_workspace_role = existing_workspace_role
|
||||||
|
new_workspace_role.role = role
|
||||||
|
except NoResultFound:
|
||||||
|
new_workspace_role = WorkspaceRole(
|
||||||
|
user=user, role_id=role.id, workspace_id=workspace_id
|
||||||
|
)
|
||||||
|
|
||||||
|
user.workspace_roles.append(new_workspace_role)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_many(cls, workspace_id, workspace_user_dicts):
|
def add_many(cls, workspace_id, workspace_user_dicts):
|
||||||
workspace_users = []
|
workspace_users = []
|
||||||
|
@ -1,90 +1,71 @@
|
|||||||
class Projects(object):
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create(self, creator_id, body):
|
from atst.database import db
|
||||||
pass
|
from atst.models.workspace import Workspace
|
||||||
|
from atst.models.workspace_role import WorkspaceRole
|
||||||
def get(self, project_id):
|
from atst.domain.exceptions import NotFoundError, UnauthorizedError
|
||||||
pass
|
from atst.domain.roles import Roles
|
||||||
|
from atst.domain.authz import Authorization
|
||||||
def get_many(self, workspace_id):
|
from atst.models.permissions import Permissions
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": "187c9bea-9541-45d7-801f-cf8e7a642e93",
|
|
||||||
"name": "Code.mil",
|
|
||||||
"environments": [
|
|
||||||
{
|
|
||||||
"id": "b1154fdd-31c9-437f-b580-2e4d757de5cb",
|
|
||||||
"name": "Development",
|
|
||||||
},
|
|
||||||
{"id": "b1e2077a-6a3d-4e7f-a80c-bf1143433adf", "name": "Sandbox"},
|
|
||||||
{
|
|
||||||
"id": "8ea95eea-7cc0-4500-adf7-8a13eaa6b752",
|
|
||||||
"name": "production",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ececfd73-b19d-45aa-9199-a950ba2c7269",
|
|
||||||
"name": "Digital Dojo",
|
|
||||||
"environments": [
|
|
||||||
{
|
|
||||||
"id": "f56167cb-ca3d-4e29-8b60-91052957a118",
|
|
||||||
"name": "Development",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7c18689c-5b77-4b68-8d64-d4d8a830bf47",
|
|
||||||
"name": "production",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def update(self, request_id, request_delta):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Members(object):
|
class Workspaces(object):
|
||||||
def __init__(self):
|
@classmethod
|
||||||
pass
|
def create(cls, request, name=None):
|
||||||
|
name = name or request.id
|
||||||
|
workspace = Workspace(request=request, name=name)
|
||||||
|
Workspaces._create_workspace_role(request.creator, workspace, "owner")
|
||||||
|
|
||||||
def create(self, creator_id, body):
|
db.session.add(workspace)
|
||||||
pass
|
db.session.commit()
|
||||||
|
|
||||||
def get(self, request_id):
|
return workspace
|
||||||
pass
|
|
||||||
|
|
||||||
def get_many(self, workspace_id):
|
@classmethod
|
||||||
return [
|
def get(cls, user, workspace_id):
|
||||||
{
|
try:
|
||||||
"first_name": "Danny",
|
workspace = db.session.query(Workspace).filter_by(id=workspace_id).one()
|
||||||
"last_name": "Knight",
|
except NoResultFound:
|
||||||
"email": "dknight@thenavy.mil",
|
raise NotFoundError("workspace")
|
||||||
"dod_id": "1257892124",
|
|
||||||
"workspace_role": "Developer",
|
|
||||||
"status": "Pending",
|
|
||||||
"num_projects": "4",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"first_name": "Mario",
|
|
||||||
"last_name": "Hudson",
|
|
||||||
"email": "mhudson@thearmy.mil",
|
|
||||||
"dod_id": "4357892125",
|
|
||||||
"workspace_role": "CCPO",
|
|
||||||
"status": "Active",
|
|
||||||
"num_projects": "0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"first_name": "Louise",
|
|
||||||
"last_name": "Greer",
|
|
||||||
"email": "lgreer@theairforce.mil",
|
|
||||||
"dod_id": "7257892125",
|
|
||||||
"workspace_role": "Admin",
|
|
||||||
"status": "Pending",
|
|
||||||
"num_projects": "43",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def update(self, request_id, request_delta):
|
if not Authorization.is_in_workspace(user, workspace):
|
||||||
pass
|
raise UnauthorizedError(user, "get workspace")
|
||||||
|
|
||||||
|
return workspace
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_update(cls, user, workspace_id):
|
||||||
|
workspace = Workspaces.get(user, workspace_id)
|
||||||
|
if not Authorization.has_workspace_permission(
|
||||||
|
user, workspace, Permissions.ADD_APPLICATION_IN_WORKSPACE
|
||||||
|
):
|
||||||
|
raise UnauthorizedError(user, "add project")
|
||||||
|
return workspace
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_request(cls, request):
|
||||||
|
try:
|
||||||
|
workspace = db.session.query(Workspace).filter_by(request=request).one()
|
||||||
|
except NoResultFound:
|
||||||
|
raise NotFoundError("workspace")
|
||||||
|
|
||||||
|
return workspace
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_many(cls, user):
|
||||||
|
workspaces = (
|
||||||
|
db.session.query(Workspace)
|
||||||
|
.join(WorkspaceRole)
|
||||||
|
.filter(WorkspaceRole.user == user)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return workspaces
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_workspace_role(cls, user, workspace, role_name):
|
||||||
|
role = Roles.get(role_name)
|
||||||
|
workspace_role = WorkspaceRole(
|
||||||
|
user=user, role=role, workspace=workspace
|
||||||
|
)
|
||||||
|
db.session.add(workspace_role)
|
||||||
|
return workspace_role
|
||||||
|
@ -71,8 +71,10 @@ class MockEDAClient(EDAClientBase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MOCK_CONTRACT_NUMBER = "DCA10096D0052"
|
||||||
|
|
||||||
def get_contract(self, contract_number, status):
|
def get_contract(self, contract_number, status):
|
||||||
if contract_number == "DCA10096D0052" and status == "y":
|
if contract_number == self.MOCK_CONTRACT_NUMBER and status == "y":
|
||||||
return {
|
return {
|
||||||
"aco_mod": "01",
|
"aco_mod": "01",
|
||||||
"admin_dodaac": None,
|
"admin_dodaac": None,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
from wtforms.fields.html5 import EmailField
|
from wtforms.fields.html5 import EmailField
|
||||||
from wtforms.fields import StringField
|
from wtforms.fields import StringField
|
||||||
from wtforms.validators import Required, Email, Regexp
|
from wtforms.validators import Required, Email, Regexp, ValidationError
|
||||||
|
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.domain.pe_numbers import PENumbers
|
from atst.domain.pe_numbers import PENumbers
|
||||||
|
from atst.domain.task_orders import TaskOrders
|
||||||
|
|
||||||
from .fields import NewlineListField, SelectField
|
from .fields import NewlineListField, SelectField
|
||||||
from .forms import ValidatedForm
|
from .forms import ValidatedForm
|
||||||
@ -57,12 +58,7 @@ def validate_pe_id(field, existing_request):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FinancialForm(ValidatedForm):
|
class BaseFinancialForm(ValidatedForm):
|
||||||
def validate(self, *args, **kwargs):
|
|
||||||
if self.funding_type.data == "OTHER":
|
|
||||||
self.funding_type_other.validators.append(Required())
|
|
||||||
return super().validate(*args, **kwargs)
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""
|
"""
|
||||||
Reset UII info so that it can be de-parsed rendered properly.
|
Reset UII info so that it can be de-parsed rendered properly.
|
||||||
@ -76,7 +72,11 @@ class FinancialForm(ValidatedForm):
|
|||||||
valid = validate_pe_id(self.pe_id, existing_request)
|
valid = validate_pe_id(self.pe_id, existing_request)
|
||||||
return valid
|
return valid
|
||||||
|
|
||||||
task_order_id = StringField(
|
@property
|
||||||
|
def is_missing_task_order_number(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
task_order_number = StringField(
|
||||||
"Task Order Number associated with this request",
|
"Task Order Number associated with this request",
|
||||||
description="Include the original Task Order number (including the 000X at the end). Do not include any modification numbers. Note that there may be a lag between approving a task order and when it becomes available in our system.",
|
description="Include the original Task Order number (including the 000X at the end). Do not include any modification numbers. Note that there may be a lag between approving a task order and when it becomes available in our system.",
|
||||||
validators=[Required()]
|
validators=[Required()]
|
||||||
@ -117,6 +117,25 @@ class FinancialForm(ValidatedForm):
|
|||||||
"Contracting Officer Representative (COR) Office", validators=[Required()]
|
"Contracting Officer Representative (COR) Office", validators=[Required()]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FinancialForm(BaseFinancialForm):
|
||||||
|
def validate_task_order_number(form, field):
|
||||||
|
try:
|
||||||
|
TaskOrders.get(field.data)
|
||||||
|
except NotFoundError:
|
||||||
|
raise ValidationError("Task Order number not found")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_missing_task_order_number(self):
|
||||||
|
return "task_order_number" in self.errors
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedFinancialForm(BaseFinancialForm):
|
||||||
|
def validate(self, *args, **kwargs):
|
||||||
|
if self.funding_type.data == "OTHER":
|
||||||
|
self.funding_type_other.validators.append(Required())
|
||||||
|
return super().validate(*args, **kwargs)
|
||||||
|
|
||||||
funding_type = SelectField(
|
funding_type = SelectField(
|
||||||
description="What is the source of funding?",
|
description="What is the source of funding?",
|
||||||
choices=[
|
choices=[
|
||||||
|
9
atst/forms/new_project.py
Normal file
9
atst/forms/new_project.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from flask_wtf import Form
|
||||||
|
from wtforms.fields import StringField, TextAreaField
|
||||||
|
|
||||||
|
|
||||||
|
class NewProjectForm(Form):
|
||||||
|
|
||||||
|
name = StringField(label="Project Name")
|
||||||
|
description = TextAreaField(label="Description")
|
||||||
|
environment_name = StringField(label="Environment Name")
|
@ -10,3 +10,6 @@ from .user import User
|
|||||||
from .workspace_role import WorkspaceRole
|
from .workspace_role import WorkspaceRole
|
||||||
from .pe_number import PENumber
|
from .pe_number import PENumber
|
||||||
from .task_order import TaskOrder
|
from .task_order import TaskOrder
|
||||||
|
from .workspace import Workspace
|
||||||
|
from .project import Project
|
||||||
|
from .environment import Environment
|
||||||
|
16
atst/models/environment.py
Normal file
16
atst/models/environment.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from atst.models import Base
|
||||||
|
from atst.models.types import Id
|
||||||
|
from atst.models.mixins import TimestampsMixin
|
||||||
|
|
||||||
|
|
||||||
|
class Environment(Base, TimestampsMixin):
|
||||||
|
__tablename__ = "environments"
|
||||||
|
|
||||||
|
id = Id()
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
|
||||||
|
project_id = Column(ForeignKey("projects.id"))
|
||||||
|
project = relationship("Project")
|
7
atst/models/mixins.py
Normal file
7
atst/models/mixins.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from sqlalchemy import Column, func, TIMESTAMP
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampsMixin(object):
|
||||||
|
time_created = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
|
||||||
|
time_updated = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now(), onupdate=func.current_timestamp())
|
||||||
|
|
18
atst/models/project.py
Normal file
18
atst/models/project.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from atst.models import Base
|
||||||
|
from atst.models.types import Id
|
||||||
|
from atst.models.mixins import TimestampsMixin
|
||||||
|
|
||||||
|
|
||||||
|
class Project(Base, TimestampsMixin):
|
||||||
|
__tablename__ = "projects"
|
||||||
|
|
||||||
|
id = Id()
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
description = Column(String, nullable=False)
|
||||||
|
|
||||||
|
workspace_id = Column(ForeignKey("workspaces.id"), nullable=False)
|
||||||
|
workspace = relationship("Workspace")
|
||||||
|
environments = relationship("Environment", back_populates="project")
|
75
atst/models/workspace.py
Normal file
75
atst/models/workspace.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from atst.models import Base
|
||||||
|
from atst.models.types import Id
|
||||||
|
from atst.models.mixins import TimestampsMixin
|
||||||
|
|
||||||
|
|
||||||
|
MOCK_MEMBERS = [
|
||||||
|
{
|
||||||
|
"first_name": "Danny",
|
||||||
|
"last_name": "Knight",
|
||||||
|
"email": "dknight@thenavy.mil",
|
||||||
|
"dod_id": "1257892124",
|
||||||
|
"workspace_role": "Developer",
|
||||||
|
"status": "Pending",
|
||||||
|
"num_projects": "4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"first_name": "Mario",
|
||||||
|
"last_name": "Hudson",
|
||||||
|
"email": "mhudson@thearmy.mil",
|
||||||
|
"dod_id": "4357892125",
|
||||||
|
"workspace_role": "CCPO",
|
||||||
|
"status": "Active",
|
||||||
|
"num_projects": "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"first_name": "Louise",
|
||||||
|
"last_name": "Greer",
|
||||||
|
"email": "lgreer@theairforce.mil",
|
||||||
|
"dod_id": "7257892125",
|
||||||
|
"workspace_role": "Admin",
|
||||||
|
"status": "Pending",
|
||||||
|
"num_projects": "43",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Workspace(Base, TimestampsMixin):
|
||||||
|
__tablename__ = "workspaces"
|
||||||
|
|
||||||
|
id = Id()
|
||||||
|
name = Column(String, unique=True)
|
||||||
|
request_id = Column(ForeignKey("requests.id"), nullable=False)
|
||||||
|
request = relationship("Request")
|
||||||
|
projects = relationship("Project", back_populates="workspace")
|
||||||
|
roles = relationship("WorkspaceRole")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self):
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
workspace_role.user
|
||||||
|
for workspace_role in self.roles
|
||||||
|
if workspace_role.role.name == "owner"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def users(self):
|
||||||
|
return set(role.user for role in self.roles)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_count(self):
|
||||||
|
return len(self.users)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def task_order(self):
|
||||||
|
return {"number": "task-order-number"}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def members(self):
|
||||||
|
return MOCK_MEMBERS
|
@ -10,11 +10,14 @@ class WorkspaceRole(Base):
|
|||||||
__tablename__ = "workspace_role"
|
__tablename__ = "workspace_role"
|
||||||
|
|
||||||
id = Id()
|
id = Id()
|
||||||
workspace_id = Column(UUID(as_uuid=True), index=True)
|
workspace_id = Column(UUID(as_uuid=True), ForeignKey("workspaces.id"), index=True)
|
||||||
|
workspace = relationship("Workspace", back_populates="roles")
|
||||||
|
|
||||||
role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
|
role_id = Column(UUID(as_uuid=True), ForeignKey("roles.id"))
|
||||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
|
||||||
role = relationship("Role")
|
role = relationship("Role")
|
||||||
|
|
||||||
|
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
||||||
|
|
||||||
|
|
||||||
Index(
|
Index(
|
||||||
"workspace_role_user_workspace",
|
"workspace_role_user_workspace",
|
||||||
|
@ -46,7 +46,10 @@ def login_redirect():
|
|||||||
user = auth_context.get_user()
|
user = auth_context.get_user()
|
||||||
session["user_id"] = user.id
|
session["user_id"] = user.id
|
||||||
|
|
||||||
return redirect(url_for("atst.home"))
|
if user.atat_role.name == "ccpo":
|
||||||
|
return redirect(url_for("atst.home"))
|
||||||
|
else:
|
||||||
|
return redirect(url_for("requests.requests_index"))
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_certificate(request):
|
def _is_valid_certificate(request):
|
||||||
|
@ -61,4 +61,8 @@ def login_dev():
|
|||||||
email=user_data["email"]
|
email=user_data["email"]
|
||||||
)
|
)
|
||||||
session["user_id"] = user.id
|
session["user_id"] = user.id
|
||||||
return redirect(url_for("atst.home"))
|
|
||||||
|
if user.atat_role.name == "ccpo":
|
||||||
|
return redirect(url_for("atst.home"))
|
||||||
|
else:
|
||||||
|
return redirect(url_for("requests.requests_index"))
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
|
from atst.domain.requests import Requests
|
||||||
|
|
||||||
requests_bp = Blueprint("requests", __name__)
|
requests_bp = Blueprint("requests", __name__)
|
||||||
|
|
||||||
from . import index
|
from . import index
|
||||||
from . import requests_form
|
from . import requests_form
|
||||||
from . import financial_verification
|
from . import financial_verification
|
||||||
|
|
||||||
|
@requests_bp.context_processor
|
||||||
|
def annual_spend_threshold():
|
||||||
|
return { "annual_spend_threshold": Requests.ANNUAL_SPEND_THRESHOLD }
|
||||||
|
@ -3,15 +3,25 @@ from flask import request as http_request
|
|||||||
|
|
||||||
from . import requests_bp
|
from . import requests_bp
|
||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
from atst.forms.financial import FinancialForm
|
from atst.forms.financial import FinancialForm, ExtendedFinancialForm
|
||||||
|
|
||||||
|
|
||||||
|
def financial_form(data):
|
||||||
|
if http_request.args.get("extended"):
|
||||||
|
return ExtendedFinancialForm(data=data)
|
||||||
|
else:
|
||||||
|
return FinancialForm(data=data)
|
||||||
|
|
||||||
|
|
||||||
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
|
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
|
||||||
def financial_verification(request_id=None):
|
def financial_verification(request_id=None):
|
||||||
request = Requests.get(request_id)
|
request = Requests.get(request_id)
|
||||||
form = FinancialForm(data=request.body.get("financial_verification"))
|
form = financial_form(request.body.get("financial_verification"))
|
||||||
return render_template(
|
return render_template(
|
||||||
"requests/financial_verification.html", f=form, request_id=request_id
|
"requests/financial_verification.html",
|
||||||
|
f=form,
|
||||||
|
request_id=request_id,
|
||||||
|
extended=http_request.args.get("extended"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -19,23 +29,27 @@ def financial_verification(request_id=None):
|
|||||||
def update_financial_verification(request_id):
|
def update_financial_verification(request_id):
|
||||||
post_data = http_request.form
|
post_data = http_request.form
|
||||||
existing_request = Requests.get(request_id)
|
existing_request = Requests.get(request_id)
|
||||||
form = FinancialForm(post_data)
|
form = financial_form(post_data)
|
||||||
|
|
||||||
rerender_args = dict(request_id=request_id, f=form)
|
rerender_args = dict(
|
||||||
|
request_id=request_id, f=form, extended=http_request.args.get("extended")
|
||||||
|
)
|
||||||
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
request_data = {"financial_verification": form.data}
|
request_data = {"financial_verification": form.data}
|
||||||
valid = form.perform_extra_validation(
|
valid = form.perform_extra_validation(
|
||||||
existing_request.body.get("financial_verification")
|
existing_request.body.get("financial_verification")
|
||||||
)
|
)
|
||||||
Requests.update(request_id, request_data)
|
updated_request = Requests.update(request_id, request_data)
|
||||||
if valid:
|
if valid:
|
||||||
return redirect(url_for("requests.financial_verification_submitted"))
|
new_workspace = Requests.approve_and_create_workspace(updated_request)
|
||||||
|
return redirect(url_for("workspaces.workspace_projects", workspace_id=new_workspace.id, newWorkspace=True))
|
||||||
else:
|
else:
|
||||||
form.reset()
|
form.reset()
|
||||||
return render_template(
|
return render_template(
|
||||||
"requests/financial_verification.html", **rerender_args
|
"requests/financial_verification.html", **rerender_args
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form.reset()
|
form.reset()
|
||||||
return render_template("requests/financial_verification.html", **rerender_args)
|
return render_template("requests/financial_verification.html", **rerender_args)
|
||||||
|
@ -1,45 +1,76 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
from atst.domain.workspaces import Projects, Members
|
render_template,
|
||||||
|
request as http_request,
|
||||||
|
g,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
|
||||||
|
from atst.domain.workspaces import Workspaces
|
||||||
|
from atst.domain.projects import Projects
|
||||||
|
from atst.domain.environments import Environments
|
||||||
|
from atst.forms.new_project import NewProjectForm
|
||||||
|
|
||||||
bp = Blueprint("workspaces", __name__)
|
bp = Blueprint("workspaces", __name__)
|
||||||
|
|
||||||
mock_workspaces = [
|
|
||||||
{
|
@bp.context_processor
|
||||||
"name": "Unclassified IaaS and PaaS for Defense Digital Service (DDS)",
|
def workspace():
|
||||||
"id": "5966187a-eff9-44c3-aa15-4de7a65ac7ff",
|
workspace = None
|
||||||
"task_order": {"number": 123456},
|
if "workspace_id" in http_request.view_args:
|
||||||
"user_count": 23,
|
workspace = Workspaces.get(
|
||||||
}
|
g.current_user, http_request.view_args["workspace_id"]
|
||||||
]
|
)
|
||||||
|
return {"workspace": workspace}
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/workspaces")
|
@bp.route("/workspaces")
|
||||||
def workspaces():
|
def workspaces():
|
||||||
return render_template("workspaces.html", page=5, workspaces=mock_workspaces)
|
workspaces = Workspaces.get_many(g.current_user)
|
||||||
|
return render_template("workspaces.html", page=5, workspaces=workspaces)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/workspaces/<workspace_id>/projects")
|
@bp.route("/workspaces/<workspace_id>/projects")
|
||||||
def workspace_projects(workspace_id):
|
def workspace_projects(workspace_id):
|
||||||
projects_repo = Projects()
|
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||||
projects = projects_repo.get_many(workspace_id)
|
return render_template("workspace_projects.html", workspace=workspace)
|
||||||
return render_template(
|
|
||||||
"workspace_projects.html", workspace_id=workspace_id, projects=projects
|
|
||||||
)
|
@bp.route("/workspaces/<workspace_id>")
|
||||||
|
def show_workspace(workspace_id):
|
||||||
|
return redirect(url_for("workspaces.workspace_projects", workspace_id=workspace_id))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/workspaces/<workspace_id>/members")
|
@bp.route("/workspaces/<workspace_id>/members")
|
||||||
def workspace_members(workspace_id):
|
def workspace_members(workspace_id):
|
||||||
members_repo = Members()
|
workspace = Workspaces.get(g.current_user, workspace_id)
|
||||||
members = members_repo.get_many(workspace_id)
|
return render_template("workspace_members.html", workspace=workspace)
|
||||||
return render_template(
|
|
||||||
"workspace_members.html", workspace_id=workspace_id, members=members
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/workspaces/<workspace_id>/reports")
|
@bp.route("/workspaces/<workspace_id>/reports")
|
||||||
def workspace_reports(workspace_id):
|
def workspace_reports(workspace_id):
|
||||||
return render_template(
|
return render_template("workspace_reports.html", workspace_id=workspace_id)
|
||||||
"workspace_reports.html", workspace_id=workspace_id
|
|
||||||
)
|
|
||||||
|
@bp.route("/workspaces/<workspace_id>/projects/new")
|
||||||
|
def new_project(workspace_id):
|
||||||
|
workspace = Workspaces.get_for_update(g.current_user, workspace_id)
|
||||||
|
form = NewProjectForm()
|
||||||
|
return render_template("workspace_project_new.html", workspace=workspace, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/workspaces/<workspace_id>/projects", methods=["POST"])
|
||||||
|
def update_project(workspace_id):
|
||||||
|
workspace = Workspaces.get_for_update(g.current_user, workspace_id)
|
||||||
|
form = NewProjectForm(http_request.form)
|
||||||
|
|
||||||
|
if form.validate():
|
||||||
|
project_data = form.data
|
||||||
|
project = Projects.create(
|
||||||
|
workspace, project_data["name"], project_data["description"]
|
||||||
|
)
|
||||||
|
Environments.create(project, project_data["environment_name"])
|
||||||
|
return redirect(
|
||||||
|
url_for("workspaces.workspace_projects", workspace_id=workspace.id)
|
||||||
|
)
|
||||||
|
@ -12,7 +12,7 @@ metadata:
|
|||||||
name: atst
|
name: atst
|
||||||
namespace: atat
|
namespace: atat
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 2
|
||||||
strategy:
|
strategy:
|
||||||
type: RollingUpdate
|
type: RollingUpdate
|
||||||
template:
|
template:
|
||||||
@ -24,10 +24,10 @@ spec:
|
|||||||
fsGroup: 101
|
fsGroup: 101
|
||||||
containers:
|
containers:
|
||||||
- name: atst
|
- name: atst
|
||||||
image: registry.atat.codes:443/atst-prod:23e5c04
|
image: registry.atat.codes:443/atst-prod:e38bc2f
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "6000Mi"
|
memory: "2500Mi"
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: atst-envvars
|
name: atst-envvars
|
||||||
|
46
js/components/forms/new_project.js
Normal file
46
js/components/forms/new_project.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import textinput from '../text_input'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'new-project',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
textinput
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
initialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
environments = ['']
|
||||||
|
} = this.initialData
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
environments,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function () {
|
||||||
|
this.$root.$on('onEnvironmentAdded', this.addEnvironment)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addEnvironment: function (event) {
|
||||||
|
this.environments.push('')
|
||||||
|
},
|
||||||
|
|
||||||
|
removeEnvironment: function (index) {
|
||||||
|
if (this.environments.length > 1) {
|
||||||
|
this.environments.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import DetailsOfUse from './components/forms/details_of_use'
|
|||||||
import poc from './components/forms/poc'
|
import poc from './components/forms/poc'
|
||||||
import financial from './components/forms/financial'
|
import financial from './components/forms/financial'
|
||||||
import toggler from './components/toggler'
|
import toggler from './components/toggler'
|
||||||
|
import NewProject from './components/forms/new_project'
|
||||||
|
|
||||||
Vue.use(VTooltip)
|
Vue.use(VTooltip)
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ const app = new Vue({
|
|||||||
DetailsOfUse,
|
DetailsOfUse,
|
||||||
poc,
|
poc,
|
||||||
financial,
|
financial,
|
||||||
|
NewProject
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeModal: function(name) {
|
closeModal: function(name) {
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
norecursedirs = .venv .git node_modules
|
norecursedirs = .venv .git node_modules
|
||||||
|
env =
|
||||||
|
D:FLASK_ENV=test
|
||||||
|
@ -8,6 +8,8 @@ sys.path.append(parent_dir)
|
|||||||
from atst.app import make_config, make_app
|
from atst.app import make_config, make_app
|
||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
from atst.domain.requests import Requests
|
from atst.domain.requests import Requests
|
||||||
|
from atst.domain.workspaces import Workspaces
|
||||||
|
from atst.domain.projects import Projects
|
||||||
from atst.domain.exceptions import AlreadyExistsError
|
from atst.domain.exceptions import AlreadyExistsError
|
||||||
from tests.factories import RequestFactory
|
from tests.factories import RequestFactory
|
||||||
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
from atst.routes.dev import _DEV_USERS as DEV_USERS
|
||||||
@ -23,11 +25,20 @@ def seed_db():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
|
requests = []
|
||||||
for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]:
|
for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]:
|
||||||
request = Requests.create(
|
request = Requests.create(
|
||||||
user, RequestFactory.build_request_body(user, dollar_value)
|
user, RequestFactory.build_request_body(user, dollar_value)
|
||||||
)
|
)
|
||||||
Requests.submit(request)
|
Requests.submit(request)
|
||||||
|
requests.append(request)
|
||||||
|
|
||||||
|
workspace = Workspaces.create(requests[0], name="{}'s workspace".format(user.first_name))
|
||||||
|
Projects.create(
|
||||||
|
workspace=workspace,
|
||||||
|
name="First Project",
|
||||||
|
description="This is our first project."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -15,11 +15,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.icon-tooltip {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin block-list__title {
|
@mixin block-list__title {
|
||||||
@include h4;
|
@include h4;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin block-list__footer {
|
@mixin block-list__footer {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
background: none;
|
background: none;
|
||||||
transition: background-color $hover-transition-time;
|
transition: background-color $hover-transition-time;
|
||||||
border-radius: $gap / 2;
|
border-radius: $gap / 2;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@include icon-color($color-primary);
|
@include icon-color($color-primary);
|
||||||
|
@ -49,4 +49,8 @@
|
|||||||
&.icon--large {
|
&.icon--large {
|
||||||
@include icon-size(24);
|
@include icon-size(24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.icon--remove {
|
||||||
|
@include icon-color($color-red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.usa-input {
|
.usa-input {
|
||||||
margin: ($gap * 4) ($gap * 2) ($gap * 4) 0;
|
margin: ($gap * 4) ($gap * 2) ($gap * 4) 0;
|
||||||
|
|
||||||
@include media($medium-screen) {
|
@include media($medium-screen) {
|
||||||
margin: ($gap * 4) 0;
|
margin: ($gap * 4) 0;
|
||||||
|
@ -63,13 +63,23 @@
|
|||||||
|
|
||||||
.panel__heading {
|
.panel__heading {
|
||||||
margin: $gap * 2;
|
margin: $gap * 2;
|
||||||
|
|
||||||
@include media($medium-screen) {
|
@include media($medium-screen) {
|
||||||
margin: $gap * 4;
|
margin: $gap * 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-tooltip {
|
||||||
|
margin-left: $gap*2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--grow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.usa-input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.project-list-item__environment__link {
|
.project-list-item__environment__link {
|
||||||
@include icon-link;
|
@include icon-link;
|
||||||
@include icon-link-large;
|
@include icon-link-large;
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
{# TODO: set this context elsewhere #}
|
|
||||||
{# set context='workspace' #}
|
|
||||||
{% set context=g.navigationContext %}
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
{% macro Tooltip(message,title='Help') -%}
|
{% macro Tooltip(message,title='Help') -%}
|
||||||
|
|
||||||
<span class="icon-tooltip" v-tooltip.top="{content: '{{message}}'}">
|
<button type="button" tabindex="0" class="icon-tooltip" v-tooltip.top="{content: '{{message}}', container: false}">
|
||||||
{{ Icon('help') }}<span>{{ title }}</span>
|
{{ Icon('help') }}<span>{{ title }}</span>
|
||||||
</span>
|
</button>
|
||||||
|
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
{# TODO: set this context elsewhere #}
|
|
||||||
{# set context='workspace' #}
|
|
||||||
{% set context=g.navigationContext %}
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% from "components/sidenav_item.html" import SidenavItem %}
|
{% from "components/sidenav_item.html" import SidenavItem %}
|
||||||
|
|
||||||
<div class="global-navigation sidenav global-navigation__context--{{context}}">
|
<div class="global-navigation sidenav">
|
||||||
<ul>
|
<ul>
|
||||||
{% if g.dev %}
|
{% if g.dev %}
|
||||||
{{ SidenavItem("Styleguide",
|
{{ SidenavItem("Styleguide",
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
{{ Icon('shield', classes='topbar__link-icon') }}
|
{{ Icon('shield', classes='topbar__link-icon') }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="topbar__context topbar__context--{{context}}">
|
<div class="topbar__context {% if workspace %}topbar__context--workspace{% endif %}">
|
||||||
<a href="/" class="topbar__link">
|
<a href="/" class="topbar__link">
|
||||||
<span class="topbar__link-label">{{ "Workspace 123456" if context == 'workspace' else "JEDI" }}</span>
|
<span class="topbar__link-label">{{ ("Workspace " + workspace.name) if workspace else "JEDI" }}</span>
|
||||||
{{ Icon('caret_down', classes='topbar__link-icon icon--tiny') }}
|
{{ Icon('caret_down', classes='topbar__link-icon icon--tiny') }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{{ SidenavItem(
|
{{ SidenavItem(
|
||||||
"Projects",
|
"Projects",
|
||||||
href=url_for("workspaces.workspace_projects", workspace_id=123456),
|
href=url_for("workspaces.workspace_projects", workspace_id=workspace.id),
|
||||||
active=g.matchesPath('\/workspaces\/[A-Za-z0-9]*\/projects'),
|
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/projects'),
|
||||||
subnav=[
|
subnav=[
|
||||||
{
|
{
|
||||||
"label": "Add New Project",
|
"label": "Add New Project",
|
||||||
"href":"/",
|
"href": url_for('workspaces.new_project', workspace_id=workspace.id),
|
||||||
"active": g.matchesPath('workspaces/projects/new'),
|
"active": g.matchesPath('\/workspaces\/[A-Za-z0-9-]*\/projects'),
|
||||||
"icon": "plus"
|
"icon": "plus"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
{{ SidenavItem(
|
{{ SidenavItem(
|
||||||
"Members",
|
"Members",
|
||||||
href=url_for("workspaces.workspace_members", workspace_id=123456),
|
href=url_for("workspaces.workspace_members", workspace_id=workspace.id),
|
||||||
active=g.matchesPath('\/workspaces\/[A-Za-z0-9]*\/members'),
|
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/members'),
|
||||||
subnav=[
|
subnav=[
|
||||||
{
|
{
|
||||||
"label": "Add New Member",
|
"label": "Add New Member",
|
||||||
@ -32,8 +32,8 @@
|
|||||||
|
|
||||||
{{ SidenavItem(
|
{{ SidenavItem(
|
||||||
"Funding & Reports",
|
"Funding & Reports",
|
||||||
href='/workspaces/123456/reports',
|
href=url_for("workspaces.workspace_reports", workspace_id=workspace.id),
|
||||||
active=g.matchesPath('\/workspaces\/[A-Za-z0-9]*\/reports')
|
active=request.url_rule.rule.startswith('/workspaces/<workspace_id>/reports')
|
||||||
) }}
|
) }}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
<footer class='block-list__footer'>
|
<footer class='block-list__footer'>
|
||||||
<a href='/' class='icon-link'>
|
<a href='/' class='icon-link'>
|
||||||
{% module Icon('plus') %}
|
{% module Icon('plus') %}
|
||||||
<span>Add another environment</span>
|
<span class="icon-link">Add another environment</span>
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
@ -9,6 +9,24 @@
|
|||||||
<financial inline-template v-bind:initial-data='{{ f.data|tojson }}'>
|
<financial inline-template v-bind:initial-data='{{ f.data|tojson }}'>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
||||||
|
{% if extended %}
|
||||||
|
{{ Alert('Task Order not found in EDA',
|
||||||
|
message="Since the Task Order (TO) number was not found in our system of record, EDA, please populate the additional fields in the form below.",
|
||||||
|
level='warning'
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if f.is_missing_task_order_number %}
|
||||||
|
{% set extended_url = url_for('requests.financial_verification', request_id=request_id, extended=True) %}
|
||||||
|
{{ Alert('Task Order not found in EDA',
|
||||||
|
message="We could not find your Task Order in our system of record, EDA.
|
||||||
|
Please confirm that you have entered it correctly.
|
||||||
|
<a href=\"%s\">Otherwise enter TO information manually.</a>
|
||||||
|
"|format(extended_url),
|
||||||
|
level='warning'
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
||||||
<div class="panel__heading">
|
<div class="panel__heading">
|
||||||
@ -19,7 +37,11 @@
|
|||||||
<div class="panel__content">
|
<div class="panel__content">
|
||||||
|
|
||||||
{% block form_action %}
|
{% block form_action %}
|
||||||
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id) }}" autocomplete="off">
|
{% if extended %}
|
||||||
|
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id, extended=True) }}" autocomplete="off">
|
||||||
|
{% else %}
|
||||||
|
<form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id) }}" autocomplete="off">
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{{ f.csrf_token }}
|
{{ f.csrf_token }}
|
||||||
@ -35,8 +57,48 @@
|
|||||||
|
|
||||||
<p>In order to get you access to the JEDI Cloud, we will need you to enter the details below that will help us verify and account for your Task Order.</p>
|
<p>In order to get you access to the JEDI Cloud, we will need you to enter the details below that will help us verify and account for your Task Order.</p>
|
||||||
|
|
||||||
|
{% if extended %}
|
||||||
|
<fieldset class="form__sub-fields form__sub-fields--warning">
|
||||||
|
{{ OptionsInput(f.funding_type) }}
|
||||||
|
|
||||||
|
<template v-if="funding_type == 'OTHER'" v-cloak>
|
||||||
|
{{ TextInput(f.funding_type_other) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
{{ TextInput(
|
||||||
|
f.clin_0001,placeholder="50,000",
|
||||||
|
validation='integer'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{{ TextInput(
|
||||||
|
f.clin_0003,placeholder="13,000",
|
||||||
|
validation='integer'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{{ TextInput(
|
||||||
|
f.clin_1001,placeholder="30,000",
|
||||||
|
validation='integer'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{{ TextInput(
|
||||||
|
f.clin_1003,placeholder="7,000",
|
||||||
|
validation='integer'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{{ TextInput(
|
||||||
|
f.clin_2001,placeholder="30,000",
|
||||||
|
validation='integer'
|
||||||
|
) }}
|
||||||
|
|
||||||
|
{{ TextInput(
|
||||||
|
f.clin_2003,placeholder="7,000",
|
||||||
|
validation='integer'
|
||||||
|
) }}
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ TextInput(
|
{{ TextInput(
|
||||||
f.task_order_id,
|
f.task_order_number,
|
||||||
placeholder="e.g.: 1234567899C0001",
|
placeholder="e.g.: 1234567899C0001",
|
||||||
tooltip="A Contracting Officer will likely be the best source for this number.",
|
tooltip="A Contracting Officer will likely be the best source for this number.",
|
||||||
validation="anything"
|
validation="anything"
|
||||||
@ -83,51 +145,6 @@
|
|||||||
|
|
||||||
{{ TextInput(f.office_cor,placeholder="e.g.: WHS") }}
|
{{ TextInput(f.office_cor,placeholder="e.g.: WHS") }}
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
{{ Alert('Task Order not found in EDA',
|
|
||||||
message="Since the Task Order (TO) number was not found in our system of record, EDA, please populate the additional fields in the form below.",
|
|
||||||
level='warning'
|
|
||||||
) }}
|
|
||||||
|
|
||||||
<fieldset class="form__sub-fields form__sub-fields--warning">
|
|
||||||
{{ OptionsInput(f.funding_type) }}
|
|
||||||
|
|
||||||
<template v-if="funding_type == 'OTHER'" v-cloak>
|
|
||||||
{{ TextInput(f.funding_type_other) }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
{{ TextInput(
|
|
||||||
f.clin_0001,placeholder="50,000",
|
|
||||||
validation='integer'
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{{ TextInput(
|
|
||||||
f.clin_0003,placeholder="13,000",
|
|
||||||
validation='integer'
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{{ TextInput(
|
|
||||||
f.clin_1001,placeholder="30,000",
|
|
||||||
validation='integer'
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{{ TextInput(
|
|
||||||
f.clin_1003,placeholder="7,000",
|
|
||||||
validation='integer'
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{{ TextInput(
|
|
||||||
f.clin_2001,placeholder="30,000",
|
|
||||||
validation='integer'
|
|
||||||
) }}
|
|
||||||
|
|
||||||
{{ TextInput(
|
|
||||||
f.clin_2003,placeholder="7,000",
|
|
||||||
validation='integer'
|
|
||||||
) }}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
|
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
{% endblock form %}
|
{% endblock form %}
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name='slide'>
|
<transition name='slide'>
|
||||||
<template v-if="annualSpend > 1000000">
|
<template v-if="annualSpend > {{ annual_spend_threshold }}">
|
||||||
<fieldset class='form__sub-fields'>
|
<fieldset class='form__sub-fields'>
|
||||||
<h3>Because the approximate annual spend is over $1,000,000, please answer a few additional questions.</h3>
|
<h3>Because the approximate annual spend is over $1,000,000, please answer a few additional questions.</h3>
|
||||||
{{ TextInput(f.number_user_sessions, validation='integer', placeholder="0") }}
|
{{ TextInput(f.number_user_sessions, validation='integer', placeholder="0") }}
|
||||||
|
@ -143,7 +143,7 @@
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if jedi_request and jedi_request.annual_spend > 1000000 %}
|
{% if jedi_request and jedi_request.annual_spend > annual_spend_threshold %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<dt>Number of User Sessions</dt>
|
<dt>Number of User Sessions</dt>
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
<h2 id="financial-verification">Financial Verification</h2>
|
<h2 id="financial-verification">Financial Verification</h2>
|
||||||
<p>In order to get you access to the JEDI Cloud, we will need you to enter the details below that will help us verify and account for your Task Order.</p>
|
<p>In order to get you access to the JEDI Cloud, we will need you to enter the details below that will help us verify and account for your Task Order.</p>
|
||||||
|
|
||||||
{{ f.task_order_id.label }}
|
{{ f.task_order_number.label }}
|
||||||
{{ f.task_order_id(placeholder="Example: 1234567899C0001") }}
|
{{ f.task_order_number(placeholder="Example: 1234567899C0001") }}
|
||||||
{% for e in f.task_order_id.errors %}
|
{% for e in f.task_order_number.errors %}
|
||||||
<div class="usa-input-error-message">
|
<div class="usa-input-error-message">
|
||||||
{{ e }}
|
{{ e }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block workspace_content %}
|
{% block workspace_content %}
|
||||||
|
|
||||||
{% if not members %}
|
{% if not workspace.members %}
|
||||||
|
|
||||||
{{ EmptyState(
|
{{ EmptyState(
|
||||||
'There are currently no members in this Workspace.',
|
'There are currently no members in this Workspace.',
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for m in members %}
|
{% for m in workspace.members %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/workspaces/123456/members/789/edit" class="icon-link icon-link--large">{{ m['first_name'] }} {{ m['last_name'] }}</a></td>
|
<td><a href="/workspaces/123456/members/789/edit" class="icon-link icon-link--large">{{ m['first_name'] }} {{ m['last_name'] }}</a></td>
|
||||||
<td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% endif %}</td>
|
<td class='table-cell--shrink'>{% if m['num_projects'] == '0' %} <span class="label label--info">No Project Access</span> {% endif %}</td>
|
||||||
|
59
templates/workspace_project_new.html
Normal file
59
templates/workspace_project_new.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/tooltip.html" import Tooltip %}
|
||||||
|
|
||||||
|
{% extends "base_workspace.html" %}
|
||||||
|
|
||||||
|
{% block workspace_content %}
|
||||||
|
<new-project inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||||
|
<form method="POST" action="{{ url_for('workspaces.update_project', workspace_id=workspace.id) }}" >
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel__heading panel__heading--grow">
|
||||||
|
<h1>Add a new project</h1>
|
||||||
|
{{ Tooltip(
|
||||||
|
"AT-AT allows you to organize your workspace into multiple projects, each of which may have environments.",
|
||||||
|
title="learn more"
|
||||||
|
)}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel__content">
|
||||||
|
{{ TextInput(form.name) }}
|
||||||
|
{{ TextInput(form.description, paragraph=True) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block-list project-list-item">
|
||||||
|
<header class="block-list__header">
|
||||||
|
<h2 class="block-list__title">Project Environments</h2>
|
||||||
|
{{ Tooltip(
|
||||||
|
"Each environment created within a project is an enclave of cloud resources that is logically separated from each other for increased security.",
|
||||||
|
title="learn more"
|
||||||
|
)}}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-for="(_, i) in environments" class="block-list__item">
|
||||||
|
{{ TextInput(form.environment_name) }}
|
||||||
|
<span class="icon-link icon-link--danger icon-link--vertical" v-on:click="removeEnvironment(i)">{{ Icon('x') }} Remove</span>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="block-list__footer">
|
||||||
|
<a v-on:click="addEnvironment" class="icon-link">Add another environment</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="action-group">
|
||||||
|
<input type="submit" value="Create Project" class="usa-button usa-button-primary">
|
||||||
|
<a href="{{ url_for('workspaces.workspace_projects', workspace_id=workspace.id) }}" class="action-group__action">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</new-project>
|
||||||
|
{% endblock %}
|
@ -1,24 +1,34 @@
|
|||||||
{% from "components/icon.html" import Icon %}
|
{% from "components/icon.html" import Icon %}
|
||||||
|
{% from "components/alert.html" import Alert %}
|
||||||
|
|
||||||
{% extends "base_workspace.html" %}
|
{% extends "base_workspace.html" %}
|
||||||
|
|
||||||
{% block workspace_content %}
|
{% block workspace_content %}
|
||||||
|
|
||||||
{% for project in projects %}
|
{% if request.args.get("newWorkspace") %}
|
||||||
|
{{ Alert('Workspace created!',
|
||||||
|
message="\
|
||||||
|
<p>You are now ready to create projects and environments within the JEDI Cloud.</p>
|
||||||
|
",
|
||||||
|
level='success'
|
||||||
|
) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for project in workspace.projects %}
|
||||||
<div class='block-list project-list-item'>
|
<div class='block-list project-list-item'>
|
||||||
<header class='block-list__header'>
|
<header class='block-list__header'>
|
||||||
<h2 class='block-list__title'>{{ project['name'] }} ({{ project['environments']|length }} environments)</h2>
|
<h2 class='block-list__title'>{{ project.name }} ({{ project.environments|length }} environments)</h2>
|
||||||
<a class='icon-link' href='/workspaces/123456/projects/789/edit'>
|
<a class='icon-link' href='/workspaces/123456/projects/789/edit'>
|
||||||
{{ Icon('edit') }}
|
{{ Icon('edit') }}
|
||||||
<span>edit</span>
|
<span>edit</span>
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
<ul>
|
<ul>
|
||||||
{% for environment in project['environments'] %}
|
{% for environment in project.environments %}
|
||||||
<li class='block-list__item project-list-item__environment'>
|
<li class='block-list__item project-list-item__environment'>
|
||||||
<a href='/' target='_blank' rel='noopener noreferrer' class='project-list-item__environment__link'>
|
<a href='/' target='_blank' rel='noopener noreferrer' class='project-list-item__environment__link'>
|
||||||
{{ Icon('link') }}
|
{{ Icon('link') }}
|
||||||
<span>{{ environment["name"]}}</span>
|
<span>{{ environment.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class='project-list-item__environment__members'>
|
<div class='project-list-item__environment__members'>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block workspace_content %}
|
{% block workspace_content %}
|
||||||
|
|
||||||
{{ Alert("Funding Information & Reports for Workspace " + workspace_id,
|
{{ Alert("Funding Information & Reports for Workspace " + workspace.name,
|
||||||
message="<p>On this screen you'll find detailed reporting information on this workspace. This message needs to be written better and be dismissable.</p>",
|
message="<p>On this screen you'll find detailed reporting information on this workspace. This message needs to be written better and be dismissable.</p>",
|
||||||
actions=[
|
actions=[
|
||||||
{"label": "Learn More", "href": "/", "icon": "info"},
|
{"label": "Learn More", "href": "/", "icon": "info"},
|
||||||
|
@ -11,16 +11,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for w in workspaces %}
|
{% for workspace in workspaces %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a class='icon-link icon-link--large' href="/workspaces/{{w['task_order']['number']}}/projects">{{ w['name'] }}</a><br>
|
<a class='icon-link icon-link--large' href="/workspaces/{{ workspace.id }}/projects">{{ workspace.name }}</a><br>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
#{{ w['task_order']['number'] }}
|
#{{ workspace.task_order.number }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="label">{{ w['user_count'] }}</span><span class='h6'>Users</span>
|
<span class="label">{{ workspace.user_count }}</span><span class='h6'>Users</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
{% extends "base.html.to" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="usa-width-one-whole empty-state">
|
|
||||||
<p>There are currently no JEDI workspaces</p>
|
|
||||||
<a href="" class="usa-button">New Workspace</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% end %}
|
|
||||||
|
|
@ -2,6 +2,7 @@ import pytest
|
|||||||
|
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
from atst.domain.task_orders import TaskOrders
|
from atst.domain.task_orders import TaskOrders
|
||||||
|
from atst.eda_client import MockEDAClient
|
||||||
|
|
||||||
from tests.factories import TaskOrderFactory
|
from tests.factories import TaskOrderFactory
|
||||||
|
|
||||||
@ -13,6 +14,19 @@ def test_can_get_task_order():
|
|||||||
assert to.id == to.id
|
assert to.id == to.id
|
||||||
|
|
||||||
|
|
||||||
def test_nonexistent_task_order_raises():
|
def test_can_get_task_order_from_eda(monkeypatch):
|
||||||
|
monkeypatch.setattr("atst.domain.task_orders.TaskOrders._client", lambda: MockEDAClient())
|
||||||
|
to = TaskOrders.get(MockEDAClient.MOCK_CONTRACT_NUMBER)
|
||||||
|
|
||||||
|
assert to.number == MockEDAClient.MOCK_CONTRACT_NUMBER
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonexistent_task_order_raises_without_client():
|
||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
TaskOrders.get("some fake number")
|
TaskOrders.get("some fake number")
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonexistent_task_order_raises_with_client(monkeypatch):
|
||||||
|
monkeypatch.setattr("atst.domain.task_orders.TaskOrders._client", lambda: MockEDAClient())
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
TaskOrders.get("some other fake numer")
|
||||||
|
@ -1,32 +1,30 @@
|
|||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from atst.domain.workspace_users import WorkspaceUsers
|
from atst.domain.workspace_users import WorkspaceUsers
|
||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
|
from tests.factories import WorkspaceFactory
|
||||||
|
|
||||||
|
|
||||||
def test_can_create_new_workspace_user():
|
def test_can_create_new_workspace_user():
|
||||||
workspace_id = uuid4()
|
workspace = WorkspaceFactory.create()
|
||||||
user = Users.create("developer")
|
new_user = Users.create("developer")
|
||||||
|
|
||||||
workspace_user_dicts = [{"id": user.id, "workspace_role": "owner"}]
|
workspace_user_dicts = [{"id": new_user.id, "workspace_role": "owner"}]
|
||||||
|
workspace_users = WorkspaceUsers.add_many(workspace.id, workspace_user_dicts)
|
||||||
|
|
||||||
workspace_users = WorkspaceUsers.add_many(workspace_id, workspace_user_dicts)
|
assert workspace_users[0].user.id == new_user.id
|
||||||
|
|
||||||
assert workspace_users[0].user.id == user.id
|
|
||||||
assert workspace_users[0].user.atat_role.name == "developer"
|
assert workspace_users[0].user.atat_role.name == "developer"
|
||||||
assert workspace_users[0].workspace_role.role.name == "owner"
|
assert workspace_users[0].workspace_role.role.name == "owner"
|
||||||
|
|
||||||
|
|
||||||
def test_can_update_existing_workspace_user():
|
def test_can_update_existing_workspace_user():
|
||||||
workspace_id = uuid4()
|
workspace = WorkspaceFactory.create()
|
||||||
user = Users.create("developer")
|
new_user = Users.create("developer")
|
||||||
|
|
||||||
WorkspaceUsers.add_many(
|
WorkspaceUsers.add_many(
|
||||||
workspace_id, [{"id": user.id, "workspace_role": "owner"}]
|
workspace.id, [{"id": new_user.id, "workspace_role": "owner"}]
|
||||||
)
|
)
|
||||||
workspace_users = WorkspaceUsers.add_many(
|
workspace_users = WorkspaceUsers.add_many(
|
||||||
workspace_id, [{"id": user.id, "workspace_role": "developer"}]
|
workspace.id, [{"id": new_user.id, "workspace_role": "developer"}]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert workspace_users[0].user.id == user.id
|
assert workspace_users[0].user.id == new_user.id
|
||||||
assert workspace_users[0].workspace_role.role.name == "developer"
|
assert workspace_users[0].workspace_role.role.name == "developer"
|
||||||
|
89
tests/domain/test_workspaces.py
Normal file
89
tests/domain/test_workspaces.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import pytest
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from atst.domain.exceptions import NotFoundError, UnauthorizedError
|
||||||
|
from atst.domain.workspaces import Workspaces
|
||||||
|
from atst.domain.workspace_users import WorkspaceUsers
|
||||||
|
|
||||||
|
from tests.factories import WorkspaceFactory, RequestFactory, UserFactory
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_create_workspace():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
workspace = Workspaces.create(request, name="frugal-whale")
|
||||||
|
assert workspace.name == "frugal-whale"
|
||||||
|
assert workspace.request == request
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_workspace_name_is_request_id():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
workspace = Workspaces.create(request)
|
||||||
|
assert workspace.name == str(request.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_nonexistent_workspace_raises():
|
||||||
|
with pytest.raises(NotFoundError):
|
||||||
|
Workspaces.get(UserFactory.build(), uuid4())
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_get_workspace_by_request():
|
||||||
|
workspace = WorkspaceFactory.create()
|
||||||
|
found = Workspaces.get_by_request(workspace.request)
|
||||||
|
assert workspace == found
|
||||||
|
|
||||||
|
|
||||||
|
def test_creating_workspace_adds_owner():
|
||||||
|
user = UserFactory.create()
|
||||||
|
request = RequestFactory.create(creator=user)
|
||||||
|
workspace = Workspaces.create(request)
|
||||||
|
assert workspace.roles[0].user == user
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspace_has_timestamps():
|
||||||
|
request = RequestFactory.create()
|
||||||
|
workspace = Workspaces.create(request)
|
||||||
|
assert workspace.time_created == workspace.time_updated
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspaces_get_ensures_user_is_in_workspace():
|
||||||
|
owner = UserFactory.create()
|
||||||
|
outside_user = UserFactory.create()
|
||||||
|
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||||
|
|
||||||
|
workspace_ = Workspaces.get(owner, workspace.id)
|
||||||
|
assert workspace_ == workspace
|
||||||
|
|
||||||
|
with pytest.raises(UnauthorizedError):
|
||||||
|
Workspaces.get(outside_user, workspace.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspaces_get_many_with_no_workspaces():
|
||||||
|
workspaces = Workspaces.get_many(UserFactory.build())
|
||||||
|
assert workspaces == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_workspaces_get_many_returns_a_users_workspaces():
|
||||||
|
user = UserFactory.create()
|
||||||
|
users_workspace = Workspaces.create(RequestFactory.create(creator=user))
|
||||||
|
|
||||||
|
# random workspace
|
||||||
|
Workspaces.create(RequestFactory.create())
|
||||||
|
|
||||||
|
assert Workspaces.get_many(user) == [users_workspace]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_for_update_allows_owner():
|
||||||
|
owner = UserFactory.create()
|
||||||
|
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||||
|
Workspaces.get_for_update(owner, workspace.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_for_update_blocks_developer():
|
||||||
|
owner = UserFactory.create()
|
||||||
|
developer = UserFactory.create()
|
||||||
|
|
||||||
|
workspace = Workspaces.create(RequestFactory.create(creator=owner))
|
||||||
|
WorkspaceUsers.add(developer, workspace.id, "developer")
|
||||||
|
|
||||||
|
with pytest.raises(UnauthorizedError):
|
||||||
|
Workspaces.get_for_update(developer, workspace.id)
|
@ -10,6 +10,7 @@ from atst.models.pe_number import PENumber
|
|||||||
from atst.models.task_order import TaskOrder
|
from atst.models.task_order import TaskOrder
|
||||||
from atst.models.user import User
|
from atst.models.user import User
|
||||||
from atst.models.role import Role
|
from atst.models.role import Role
|
||||||
|
from atst.models.workspace import Workspace
|
||||||
from atst.models.request_status_event import RequestStatusEvent
|
from atst.models.request_status_event import RequestStatusEvent
|
||||||
from atst.domain.roles import Roles
|
from atst.domain.roles import Roles
|
||||||
|
|
||||||
@ -102,3 +103,12 @@ class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory):
|
|||||||
class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory):
|
class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaskOrder
|
model = TaskOrder
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = Workspace
|
||||||
|
|
||||||
|
request = factory.SubFactory(RequestFactory)
|
||||||
|
# name it the same as the request ID by default
|
||||||
|
name = factory.LazyAttribute(lambda w: w.request.id)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from atst.forms.financial import suggest_pe_id, FinancialForm
|
from atst.forms.financial import suggest_pe_id, FinancialForm, ExtendedFinancialForm
|
||||||
|
from atst.eda_client import MockEDAClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("input_,expected", [
|
@pytest.mark.parametrize("input_,expected", [
|
||||||
@ -18,7 +19,7 @@ def test_funding_type_other_not_required_if_funding_type_is_not_other():
|
|||||||
form_data = {
|
form_data = {
|
||||||
"funding_type": "PROC"
|
"funding_type": "PROC"
|
||||||
}
|
}
|
||||||
form = FinancialForm(data=form_data)
|
form = ExtendedFinancialForm(data=form_data)
|
||||||
form.validate()
|
form.validate()
|
||||||
assert "funding_type_other" not in form.errors
|
assert "funding_type_other" not in form.errors
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ def test_funding_type_other_required_if_funding_type_is_other():
|
|||||||
form_data = {
|
form_data = {
|
||||||
"funding_type": "OTHER"
|
"funding_type": "OTHER"
|
||||||
}
|
}
|
||||||
form = FinancialForm(data=form_data)
|
form = ExtendedFinancialForm(data=form_data)
|
||||||
form.validate()
|
form.validate()
|
||||||
assert "funding_type_other" in form.errors
|
assert "funding_type_other" in form.errors
|
||||||
|
|
||||||
@ -67,3 +68,16 @@ def test_ba_code_validation(input_, expected):
|
|||||||
is_valid = "ba_code" not in form.errors
|
is_valid = "ba_code" not in form.errors
|
||||||
|
|
||||||
assert is_valid == expected
|
assert is_valid == expected
|
||||||
|
|
||||||
|
def test_task_order_number_validation(monkeypatch):
|
||||||
|
monkeypatch.setattr("atst.domain.task_orders.TaskOrders._client", lambda: MockEDAClient())
|
||||||
|
form_invalid = FinancialForm(data={"task_order_number": "1234"})
|
||||||
|
form_invalid.validate()
|
||||||
|
|
||||||
|
assert "task_order_number" in form_invalid.errors
|
||||||
|
|
||||||
|
form_valid = FinancialForm(data={"task_order_number": MockEDAClient.MOCK_CONTRACT_NUMBER}, eda_client=MockEDAClient())
|
||||||
|
form_valid.validate()
|
||||||
|
|
||||||
|
assert "task_order_number" not in form_valid.errors
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import re
|
|
||||||
import pytest
|
|
||||||
import urllib
|
import urllib
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from atst.eda_client import MockEDAClient
|
||||||
|
|
||||||
from tests.mocks import MOCK_REQUEST, MOCK_USER
|
from tests.mocks import MOCK_REQUEST, MOCK_USER
|
||||||
from tests.factories import PENumberFactory
|
from tests.factories import PENumberFactory, RequestFactory
|
||||||
|
|
||||||
|
|
||||||
class TestPENumberInForm:
|
class TestPENumberInForm:
|
||||||
|
|
||||||
required_data = {
|
required_data = {
|
||||||
"pe_id": "123",
|
"pe_id": "123",
|
||||||
"task_order_id": "1234567899C0001",
|
"task_order_number": MockEDAClient.MOCK_CONTRACT_NUMBER,
|
||||||
"fname_co": "Contracting",
|
"fname_co": "Contracting",
|
||||||
"lname_co": "Officer",
|
"lname_co": "Officer",
|
||||||
"email_co": "jane@mail.mil",
|
"email_co": "jane@mail.mil",
|
||||||
@ -18,6 +20,11 @@ class TestPENumberInForm:
|
|||||||
"lname_cor": "Representative",
|
"lname_cor": "Representative",
|
||||||
"email_cor": "jane@mail.mil",
|
"email_cor": "jane@mail.mil",
|
||||||
"office_cor": "WHS",
|
"office_cor": "WHS",
|
||||||
|
"uii_ids": "1234",
|
||||||
|
"treasury_code": "00123456",
|
||||||
|
"ba_code": "024A"
|
||||||
|
}
|
||||||
|
extended_data = {
|
||||||
"funding_type": "RDTE",
|
"funding_type": "RDTE",
|
||||||
"funding_type_other": "other",
|
"funding_type_other": "other",
|
||||||
"clin_0001": "50,000",
|
"clin_0001": "50,000",
|
||||||
@ -30,12 +37,15 @@ class TestPENumberInForm:
|
|||||||
|
|
||||||
def _set_monkeypatches(self, monkeypatch):
|
def _set_monkeypatches(self, monkeypatch):
|
||||||
monkeypatch.setattr("atst.forms.financial.FinancialForm.validate", lambda s: True)
|
monkeypatch.setattr("atst.forms.financial.FinancialForm.validate", lambda s: True)
|
||||||
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
|
|
||||||
monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: MOCK_USER)
|
monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: MOCK_USER)
|
||||||
|
|
||||||
def submit_data(self, client, data):
|
def submit_data(self, client, data, extended=False):
|
||||||
|
request = RequestFactory.create(body=MOCK_REQUEST.body)
|
||||||
|
url_kwargs = {"request_id": request.id}
|
||||||
|
if extended:
|
||||||
|
url_kwargs["extended"] = True
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/requests/verify/{}".format(MOCK_REQUEST.id),
|
url_for("requests.financial_verification", **url_kwargs),
|
||||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
data=urllib.parse.urlencode(data),
|
data=urllib.parse.urlencode(data),
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
@ -59,7 +69,7 @@ class TestPENumberInForm:
|
|||||||
response = self.submit_data(client, data)
|
response = self.submit_data(client, data)
|
||||||
|
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
assert "/workspaces" in response.headers.get("Location")
|
||||||
|
|
||||||
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, client):
|
def test_submit_request_form_with_new_valid_pe_id(self, monkeypatch, client):
|
||||||
self._set_monkeypatches(monkeypatch)
|
self._set_monkeypatches(monkeypatch)
|
||||||
@ -71,7 +81,7 @@ class TestPENumberInForm:
|
|||||||
response = self.submit_data(client, data)
|
response = self.submit_data(client, data)
|
||||||
|
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
assert "/requests/financial_verification_submitted" in response.headers.get("Location")
|
assert "/workspaces" in response.headers.get("Location")
|
||||||
|
|
||||||
def test_submit_request_form_with_missing_pe_id(self, monkeypatch, client):
|
def test_submit_request_form_with_missing_pe_id(self, monkeypatch, client):
|
||||||
self._set_monkeypatches(monkeypatch)
|
self._set_monkeypatches(monkeypatch)
|
||||||
@ -83,3 +93,40 @@ class TestPENumberInForm:
|
|||||||
|
|
||||||
assert "There were some errors" in response.data.decode()
|
assert "There were some errors" in response.data.decode()
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_submit_financial_form_with_invalid_task_order(self, monkeypatch, user_session, client):
|
||||||
|
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
|
||||||
|
user_session()
|
||||||
|
|
||||||
|
data = dict(self.required_data)
|
||||||
|
data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id']
|
||||||
|
data['task_order_number'] = '1234'
|
||||||
|
|
||||||
|
response = self.submit_data(client, data)
|
||||||
|
|
||||||
|
assert "enter TO information manually" in response.data.decode()
|
||||||
|
|
||||||
|
def test_submit_financial_form_with_valid_task_order(self, monkeypatch, user_session, client):
|
||||||
|
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
|
||||||
|
user_session()
|
||||||
|
|
||||||
|
data = dict(self.required_data)
|
||||||
|
data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id']
|
||||||
|
data['task_order_number'] = MockEDAClient.MOCK_CONTRACT_NUMBER
|
||||||
|
|
||||||
|
response = self.submit_data(client, data)
|
||||||
|
|
||||||
|
assert "enter TO information manually" not in response.data.decode()
|
||||||
|
|
||||||
|
def test_submit_extended_financial_form(self, monkeypatch, user_session, client):
|
||||||
|
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda i: MOCK_REQUEST)
|
||||||
|
user_session()
|
||||||
|
|
||||||
|
data = { **self.required_data, **self.extended_data }
|
||||||
|
data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id']
|
||||||
|
data['task_order_number'] = "1234567"
|
||||||
|
|
||||||
|
response = self.submit_data(client, data, extended=True)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert "/workspaces" in response.headers.get("Location")
|
||||||
|
@ -2,6 +2,7 @@ import pytest
|
|||||||
from flask import session, url_for
|
from flask import session, url_for
|
||||||
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
|
from .mocks import DOD_SDN_INFO, DOD_SDN, FIXTURE_EMAIL_ADDRESS
|
||||||
from atst.domain.users import Users
|
from atst.domain.users import Users
|
||||||
|
from atst.domain.roles import Roles
|
||||||
from atst.domain.exceptions import NotFoundError
|
from atst.domain.exceptions import NotFoundError
|
||||||
from .factories import UserFactory
|
from .factories import UserFactory
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ def _fetch_user_info(c, t):
|
|||||||
return MOCK_USER
|
return MOCK_USER
|
||||||
|
|
||||||
|
|
||||||
def test_successful_login_redirect(client, monkeypatch):
|
def test_successful_login_redirect_non_ccpo(client, monkeypatch):
|
||||||
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True)
|
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True)
|
||||||
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.get_user", lambda *args: UserFactory.create())
|
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.get_user", lambda *args: UserFactory.create())
|
||||||
|
|
||||||
@ -26,6 +27,24 @@ def test_successful_login_redirect(client, monkeypatch):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "requests" in resp.headers["Location"]
|
||||||
|
assert session["user_id"]
|
||||||
|
|
||||||
|
def test_successful_login_redirect_ccpo(client, monkeypatch):
|
||||||
|
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.authenticate", lambda *args: True)
|
||||||
|
role = Roles.get("ccpo")
|
||||||
|
monkeypatch.setattr("atst.domain.authnid.AuthenticationContext.get_user", lambda *args: UserFactory.create(atat_role=role))
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
"/login-redirect",
|
||||||
|
environ_base={
|
||||||
|
"HTTP_X_SSL_CLIENT_VERIFY": "SUCCESS",
|
||||||
|
"HTTP_X_SSL_CLIENT_S_DN": "",
|
||||||
|
"HTTP_X_SSL_CLIENT_CERT": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "home" in resp.headers["Location"]
|
assert "home" in resp.headers["Location"]
|
||||||
assert session["user_id"]
|
assert session["user_id"]
|
||||||
@ -90,7 +109,7 @@ def test_crl_validation_on_login(client):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "home" in resp.headers["Location"]
|
assert "requests" in resp.headers["Location"]
|
||||||
assert session["user_id"]
|
assert session["user_id"]
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user