Merge pull request #119 from dod-ccpo/flask2-rebase

Use flask instead of tornado
This commit is contained in:
dandds 2018-08-02 16:34:40 -04:00 committed by GitHub
commit 32cc6cb520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2302 additions and 1451 deletions

14
Pipfile
View File

@ -4,26 +4,30 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
tornado = "==5.0.2" tornado = "*"
webassets = "==0.12.1" webassets = "*"
Unipath = "==1.1" Unipath = "*"
wtforms-tornado = "*" wtforms-tornado = "*"
pendulum = "*" pendulum = "*"
redis = "*" redis = "*"
sqlalchemy = "*" sqlalchemy = "*"
alembic = "*" alembic = "*"
"psycopg2-binary" = "*" "psycopg2-binary" = "*"
flask = "*"
flask-sqlalchemy = "*"
flask-assets = "*"
flask-session = "*"
[dev-packages] [dev-packages]
bandit = "*" bandit = "*"
pytest = "==3.6.0" pytest = "*"
pytest-tornado = "==0.5.0"
ipython = "*" ipython = "*"
ipdb = "*" ipdb = "*"
pylint = "*" pylint = "*"
black = "*" black = "*"
pytest-watch = "*" pytest-watch = "*"
factory-boy = "*" factory-boy = "*"
pytest-flask = "*"
[requires] [requires]
python_version = "3.6" python_version = "3.6"

244
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "7cd87f2c2c42bc776a6aa6f72fcbb8b30d4e703e50b6480ce1c8ace6ae6dd0a4" "sha256": "9f17530cb96833c424369b9cac305cb43a817cdf19605aaedeb2d98566302857"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -24,6 +24,57 @@
"index": "pypi", "index": "pypi",
"version": "==1.0.0" "version": "==1.0.0"
}, },
"click": {
"hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
],
"version": "==6.7"
},
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"flask-assets": {
"hashes": [
"sha256:6031527b89fb3509d1581d932affa5a79dd348cfffb58d0aef99a43461d47847"
],
"index": "pypi",
"version": "==0.12"
},
"flask-session": {
"hashes": [
"sha256:a31c27e0c3287f00c825b3d9625aba585f4df4cccedb1e7dd5a69a215881a731",
"sha256:b9b32126bfc52c3169089f2ed9a40e34b589527bda48b633428e07d39d9c8792"
],
"index": "pypi",
"version": "==0.3.1"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b",
"sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"
],
"index": "pypi",
"version": "==2.3.2"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"mako": { "mako": {
"hashes": [ "hashes": [
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" "sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
@ -38,19 +89,22 @@
}, },
"pendulum": { "pendulum": {
"hashes": [ "hashes": [
"sha256:0643d45824e6789b88187728337dfa6075a0233f6976c2abefba00d064156309", "sha256:0ec5371949e147753661e1e98721273170638034dfceb578f29d69d93d3d474b",
"sha256:3cc271195d8054bec06f54ff7d56ea6c2e2b5ad5dd6b532d787b34d2cabe6a65", "sha256:10ccdc8c6d004ba97883dd0f57503963ddf6cb83e849a16c4675ba18da657564",
"sha256:544e44d8a92954e5ef4db4fa8b662d3282f2ac7b7c2cbf4227dc193ba78b9e1e", "sha256:37bb54bcbb9d7fccd725f3fda69702e51ab3de9971b4c1c986505fbb3bc58bed",
"sha256:846478ab5f7480b3d850a09e44fe03830d448633c84f0b1066615ff6c34293aa", "sha256:51803352e40778f914ff7af3494788b404260b415d9a9d607a8cf73e5e120994",
"sha256:8bb523f759daeecfc0649369f198cbeb27a6608347354f4f847d21d579003db6", "sha256:5de295ca85761d9adf4020e6f3bed6eb933846ccf23b74e04b071f6d677f11a4",
"sha256:a449142063100f1b3c1119453c7569667c9ba79897305a1c50ca83a8c790f1e4", "sha256:73f850265adcf0986fcc0af83ae9c8c5a7ca3c4a2525184110478a8bfd1a77b3",
"sha256:b7ff156b3d7cccbdeeb63465578d9a4e6f57d463f6ff6d4474254208d08f8353", "sha256:8fe289356322f6b0f4510082b4c412a1496a64054a37ae86b24411868a1901c6",
"sha256:d8822a592bbc16576c44ec4625bff9187ed9b649d47714e4905a55adc5b25339", "sha256:c0401482dfa9fbd7005f2dfbf54ec61fd2c8130df37651ac2a3722d1f049ae4e",
"sha256:dd45c7b349faab69714df9835cdf8bf8bce50bf6fc471419d3b23ba33e1915a5", "sha256:c358ee65ddb99c2b1bf301458e43ed09ff6d40465bcc9928265246912fad4d0f",
"sha256:fac088b637b5db5a047a0e89194d8c3c9e9e9ce1665089240003bb7c05b92536" "sha256:d07962450e808556b3e6209a5830e2bbf8c7747129580c3b5b09e641f72617ab",
"sha256:dc05e6186c9c3b9969326aded9cba7a796744918581b25457f5148a5e3475d55",
"sha256:ee9466eea403e8e308c284d3055e285b97905a5ffb1566df0ef200b4f39c0f15",
"sha256:f7fa6220251a636112721e8158b9dd59018d818ec121047900934d80864eca62"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.2" "version": "==2.0.3"
}, },
"psycopg2-binary": { "psycopg2-binary": {
"hashes": [ "hashes": [
@ -106,7 +160,6 @@
"sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622",
"sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5" "sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5"
], ],
"markers": "python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7'",
"version": "==2018.5" "version": "==2018.5"
}, },
"redis": { "redis": {
@ -133,14 +186,16 @@
}, },
"tornado": { "tornado": {
"hashes": [ "hashes": [
"sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7", "sha256:1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448",
"sha256:408d129e9d13d3c55aa73f8084aa97d5f90ed84132e38d6932e63a67d5bec563", "sha256:4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582",
"sha256:88ce0282cce70df9045e515f578c78f1ebc35dcabe1d70f800c3583ebda7f5f5", "sha256:5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046",
"sha256:ba9fbb249ac5390bff8a1d6aa4b844fd400701069bda7d2e380dfe2217895101", "sha256:6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9",
"sha256:c050089173c2e9272244bccfb6a8615fb9e53b79420a5551acfa76094ecc3111" "sha256:a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f",
"sha256:c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866",
"sha256:d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.0.2" "version": "==5.1"
}, },
"unipath": { "unipath": {
"hashes": [ "hashes": [
@ -157,6 +212,13 @@
"index": "pypi", "index": "pypi",
"version": "==0.12.1" "version": "==0.12.1"
}, },
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
},
"wtforms": { "wtforms": {
"hashes": [ "hashes": [
"sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61", "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61",
@ -197,10 +259,10 @@
}, },
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", "sha256:a48b57ede295c3188ef5c84273bc2a8eadc46e4cbb001eae0d49fb5d1fabbb19",
"sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" "sha256:d066cdeec5faeb51a4be5010da612680653d844b57afd86a5c8315f2f801b4cc"
], ],
"version": "==2.0.1" "version": "==2.0.2"
}, },
"atomicwrites": { "atomicwrites": {
"hashes": [ "hashes": [
@ -279,9 +341,16 @@
"sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867", "sha256:0e9a1227a3a0f3297a485715e72ee6eb77081b17b629367042b586e38c03c867",
"sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d" "sha256:b4840807a94a3bad0217d6ed3f9b65a1cc6e1db1c99e1184673056ae2c0a4c4d"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*'",
"version": "==0.8.17" "version": "==0.8.17"
}, },
"flask": {
"hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
],
"index": "pypi",
"version": "==1.0.2"
},
"gitdb2": { "gitdb2": {
"hashes": [ "hashes": [
"sha256:87783b7f4a8f6b71c7fe81d32179b3c8781c1a7d6fa0c69bff2f315b00aff4f8", "sha256:87783b7f4a8f6b71c7fe81d32179b3c8781c1a7d6fa0c69bff2f315b00aff4f8",
@ -305,11 +374,11 @@
}, },
"ipython": { "ipython": {
"hashes": [ "hashes": [
"sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", "sha256:007dcd929c14631f83daff35df0147ea51d1af420da303fd078343878bd5fb62",
"sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c" "sha256:b0f2ef9eada4a68ef63ee10b6dde4f35c840035c50fd24265f8052c98947d5a4"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.4.0" "version": "==6.5.0"
}, },
"ipython-genutils": { "ipython-genutils": {
"hashes": [ "hashes": [
@ -324,9 +393,14 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
], ],
"markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==4.3.4" "version": "==4.3.4"
}, },
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
],
"version": "==0.24"
},
"jedi": { "jedi": {
"hashes": [ "hashes": [
"sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1",
@ -334,6 +408,13 @@
], ],
"version": "==0.12.1" "version": "==0.12.1"
}, },
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"lazy-object-proxy": { "lazy-object-proxy": {
"hashes": [ "hashes": [
"sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
@ -368,6 +449,12 @@
], ],
"version": "==1.3.1" "version": "==1.3.1"
}, },
"markupsafe": {
"hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
},
"mccabe": { "mccabe": {
"hashes": [ "hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
@ -377,11 +464,11 @@
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8", "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3", "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0" "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
], ],
"version": "==4.2.0" "version": "==4.3.0"
}, },
"parso": { "parso": {
"hashes": [ "hashes": [
@ -398,10 +485,10 @@
}, },
"pbr": { "pbr": {
"hashes": [ "hashes": [
"sha256:754e766b4f4bad3aa68cfd532456298da1aa39375da8748392dbae90860d5f18", "sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45",
"sha256:c6bddbad814f23c7faaf88d8a186e9965243cc6206a23361b73023648e645794" "sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa"
], ],
"version": "==4.1.1" "version": "==4.2.0"
}, },
"pexpect": { "pexpect": {
"hashes": [ "hashes": [
@ -420,12 +507,10 @@
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
"sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5"
], ],
"markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==0.7.1"
"version": "==0.6.0"
}, },
"prompt-toolkit": { "prompt-toolkit": {
"hashes": [ "hashes": [
@ -447,7 +532,6 @@
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
], ],
"markers": "python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*'",
"version": "==1.5.4" "version": "==1.5.4"
}, },
"pygments": { "pygments": {
@ -459,27 +543,27 @@
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434", "sha256:0edfec21270725c5aa8e8d8d06ef5666f766e0e748ed2f1ab23624727303b935",
"sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251" "sha256:4cadcaa4f1fb19123d4baa758d9fbe6286c5b3aa513af6ea42a2d51d405db205"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.1" "version": "==2.1.0"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:39555d023af3200d004d09e51b4dd9fdd828baa863cded3fd6ba2f29f757ae2d", "sha256:8214ab8446104a1d0c17fbd218ec6aac743236c6ffbe23abc038e40213c60b88",
"sha256:c76e93f3145a44812955e8d46cdd302d8a45fbfc7bf22be24fe231f9d8d8853a" "sha256:e2b2c6e1560b8f9dc8dd600b0923183fbd68ba3d9bdecde04467be6dd296a384"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.6.0" "version": "==3.7.0"
}, },
"pytest-tornado": { "pytest-flask": {
"hashes": [ "hashes": [
"sha256:214fc59d06fb81696fce3028b56dff522168ac1cfc784cfc0077b7b1e425b4cd", "sha256:2c5a36f9033ef8b6f85ddbefaebdd4f89197fc283f94b20dfe1a1beba4b77f03",
"sha256:687c1f9c0f5bda7808c1e53c14bbebfe4fb9452e34cc95b440e598d4724265e0" "sha256:657c7de386215ab0230bee4d76ace0339ae82fcbb34e134e17a29f65032eef03"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.5.0" "version": "==0.10.0"
}, },
"pytest-watch": { "pytest-watch": {
"hashes": [ "hashes": [
@ -497,11 +581,15 @@
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110",
"sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb",
"sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2",
"sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76",
"sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7",
"sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" "sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8",
"sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b",
"sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d"
], ],
"version": "==4.2b4" "version": "==4.2b4"
}, },
@ -545,17 +633,6 @@
], ],
"version": "==0.9.4" "version": "==0.9.4"
}, },
"tornado": {
"hashes": [
"sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7",
"sha256:408d129e9d13d3c55aa73f8084aa97d5f90ed84132e38d6932e63a67d5bec563",
"sha256:88ce0282cce70df9045e515f578c78f1ebc35dcabe1d70f800c3583ebda7f5f5",
"sha256:ba9fbb249ac5390bff8a1d6aa4b844fd400701069bda7d2e380dfe2217895101",
"sha256:c050089173c2e9272244bccfb6a8615fb9e53b79420a5551acfa76094ecc3111"
],
"index": "pypi",
"version": "==5.0.2"
},
"traitlets": { "traitlets": {
"hashes": [ "hashes": [
"sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
@ -563,6 +640,42 @@
], ],
"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"
@ -576,6 +689,13 @@
], ],
"version": "==0.1.7" "version": "==0.1.7"
}, },
"werkzeug": {
"hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
],
"version": "==0.14.1"
},
"wrapt": { "wrapt": {
"hashes": [ "hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"

View File

@ -72,6 +72,12 @@ To log in as one of them, navigate to `/login-dev?username=<lowercase name>`. Fo
## Testing ## Testing
Tests require a test database:
```
createdb atat_test
```
To run lint, static analysis, and unit tests: To run lint, static analysis, and unit tests:
script/test script/test

View File

@ -30,11 +30,10 @@ sys.path.append(parent_dir)
from atst.app import make_config from atst.app import make_config
app_config = make_config() app_config = make_config()
config.set_main_option('sqlalchemy.url', app_config['default']['DATABASE_URI']) config.set_main_option('sqlalchemy.url', app_config['DATABASE_URI'])
from atst.database import make_db from atst.database import db
from atst.models import * from atst.models import *
db = make_db(app_config)
target_metadata = Base.metadata target_metadata = Base.metadata

12
app.py
View File

@ -1,15 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
import tornado.ioloop from atst.app import make_app, make_config
from atst.app import make_app, make_deps, make_config
config = make_config() config = make_config()
deps = make_deps(config) app = make_app(config)
app = make_app(config, deps)
if __name__ == "__main__": if __name__ == "__main__":
port = int(config["default"]["PORT"]) port = int(config["PORT"])
app.listen(port) app.run(port=port)
print("Listening on http://localhost:%i" % port) print("Listening on http://localhost:%i" % port)
tornado.ioloop.IOLoop.current().start()

View File

@ -1,176 +1,71 @@
import os import os
import re
from configparser import ConfigParser from configparser import ConfigParser
import tornado.web from flask import Flask, request, g
from tornado.web import url from unipath import Path
from redis import StrictRedis
from atst.handlers.main import Main from atst.database import db
from atst.handlers.root import Root from atst.assets import environment as assets_environment
from atst.handlers.login_redirect import LoginRedirect
from atst.handlers.workspaces import Workspaces
from atst.handlers.workspace import Workspace
from atst.handlers.workspace_members import WorkspaceMembers
from atst.handlers.request import Request
from atst.handlers.request_financial_verification import RequestFinancialVerification
from atst.handlers.request_new import RequestNew
from atst.handlers.request_submit import RequestsSubmit
from atst.handlers.dev import Dev
from atst.home import home
from atst.api_client import ApiClient
from atst.sessions import RedisSessions
from atst import ui_modules
from atst import ui_methods
from atst.database import make_db
ENV = os.getenv("TORNADO_ENV", "dev") from atst.routes import bp
from atst.routes.workspaces import bp as workspace_routes
from atst.routes.requests import requests_bp
def make_app(config, deps, **kwargs): ENV = os.getenv("FLASK_ENV", "dev")
routes = [
url(r"/", Root, {"page": "root"}, name="root"),
url(
r"/login-redirect",
LoginRedirect,
{
"sessions": deps["sessions"],
"authnid_client": deps["authnid_client"],
"db_session": deps["db_session"],
},
name="login_redirect",
),
url(r"/home", Main, {"page": "home"}, name="home"),
url(r"/styleguide", Main, {"page": "styleguide"}, name="styleguide"),
url(
r"/workspaces/blank",
Main,
{"page": "workspaces_blank"},
name="workspaces_blank",
),
url(
r"/workspaces",
Workspaces,
{"page": "workspaces", "db_session": deps["db_session"]},
name="workspaces",
),
url(
r"/requests",
Request,
{"page": "requests", "db_session": deps["db_session"]},
name="requests",
),
url(
r"/requests/new",
RequestNew,
{
"page": "requests_new",
"db_session": deps["db_session"],
},
name="request_new",
),
url(
r"/requests/new/([0-9])",
RequestNew,
{
"page": "requests_new",
"db_session": deps["db_session"],
},
name="request_form_new",
),
url(
r"/requests/new/([0-9])/(\S+)",
RequestNew,
{
"page": "requests_new",
"db_session": deps["db_session"],
},
name="request_form_update",
),
url(
r"/requests/submit/(\S+)",
RequestsSubmit,
{"db_session": deps["db_session"]},
name="requests_submit",
),
# Dummy request/approval screen
url(
r"/request/approval",
Main,
{"page": "request_approval"},
name="request_approval",
),
url(
r"/requests/verify/(\S+)",
RequestFinancialVerification,
{
"page": "financial_verification",
"db_session": deps["db_session"],
},
name="financial_verification",
),
url(
r"/requests/financial_verification_submitted",
Main,
{"page": "requests/financial_verification_submitted"},
name="financial_verification_submitted",
),
url(r"/users", Main, {"page": "users"}, name="users"),
url(r"/reports", Main, {"page": "reports"}, name="reports"),
url(r"/calculator", Main, {"page": "calculator"}, name="calculator"),
url(
r"/workspaces/(\S+)/members", WorkspaceMembers, {}, name="workspace_members"
),
url(r"/workspaces/(\S+)/projects", Workspace, {}, name="workspace_projects"),
url(r"/workspaces/123456/projects/789/edit", Main, {"page": "project_edit"}, name="project_edit"),
]
if not ENV == "production": def make_app(config):
routes += [
url(
r"/login-dev",
Dev,
{
"action": "login",
"sessions": deps["sessions"],
"db_session": deps["db_session"],
},
name="dev-login",
)
]
app = tornado.web.Application( parent_dir = Path().parent
routes,
login_url="/", app = Flask(
template_path=home.child("templates"), __name__,
static_path=home.child("static"), template_folder=parent_dir.child("templates").absolute(),
cookie_secret=config["default"]["COOKIE_SECRET"], static_folder=parent_dir.child("static").absolute(),
debug=config["default"].getboolean("DEBUG"),
ui_modules=ui_modules,
ui_methods=ui_methods,
**kwargs
) )
app.config = config app.config.update(config)
app.sessions = deps["sessions"]
make_flask_callbacks(app)
db.init_app(app)
assets_environment.init_app(app)
app.register_blueprint(bp)
app.register_blueprint(workspace_routes)
app.register_blueprint(requests_bp)
return app return app
def make_deps(config): def make_flask_callbacks(app):
# we do not want to do SSL verify services in test and development @app.before_request
validate_cert = ENV == "production" def _set_globals():
redis_client = StrictRedis.from_url( g.navigationContext = (
config["default"]["REDIS_URI"], decode_responses=True "workspace"
) if re.match("\/workspaces\/[A-Za-z0-9]*", request.url)
else "global"
)
g.dev = os.getenv("TORNADO_ENV", "dev") == "dev"
g.matchesPath = lambda href: re.match("^" + href, request.path)
g.modalOpen = request.args.get("modal", False)
g.current_user = {
"id": "cce17030-4109-4719-b958-ed109dbb87c8",
"first_name": "Amanda",
"last_name": "Adamson",
"atat_role": "default",
"atat_permissions": [],
}
def map_config(config):
return { return {
"db_session": make_db(config), "ENV": config["default"]["ENVIRONMENT"],
"authnid_client": ApiClient( "DEBUG": config["default"]["DEBUG"],
config["default"]["AUTHNID_BASE_URL"], "PORT": int(config["default"]["PORT"]),
api_version="v1", "SQLALCHEMY_DATABASE_URI": config["default"]["DATABASE_URI"],
validate_cert=validate_cert, "SQLALCHEMY_TRACK_MODIFICATIONS": False,
), **config["default"],
"sessions": RedisSessions(
redis_client, config["default"]["SESSION_TTL_SECONDS"]
),
} }
@ -182,6 +77,11 @@ def make_config():
OVERRIDE_CONFIG_FILENAME = os.getenv("OVERRIDE_CONFIG_FULLPATH") OVERRIDE_CONFIG_FILENAME = os.getenv("OVERRIDE_CONFIG_FULLPATH")
config = ConfigParser() config = ConfigParser()
config.optionxform = str
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
if OVERRIDE_CONFIG_FILENAME:
config_files.append(OVERRIDE_CONFIG_FILENAME)
config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME] config_files = [BASE_CONFIG_FILENAME, ENV_CONFIG_FILENAME]
if OVERRIDE_CONFIG_FILENAME: if OVERRIDE_CONFIG_FILENAME:
@ -205,4 +105,4 @@ def make_config():
) )
config.set("default", "DATABASE_URI", database_uri) config.set("default", "DATABASE_URI", database_uri)
return config return map_config(config)

View File

@ -1,13 +1,9 @@
from webassets import Environment, Bundle from flask_assets import Environment, Bundle
from atst.home import home
environment = Environment( environment = Environment()
directory=home.child("scss"),
url="/static"
)
css = Bundle( css = Bundle(
"atat.scss", "../scss/atat.scss",
filters="scss", filters="scss",
output="../static/assets/out.%(version)s.css", output="../static/assets/out.%(version)s.css",
depends=("**/*.scss"), depends=("**/*.scss"),

View File

@ -1,8 +1,3 @@
from sqlalchemy import create_engine from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import sessionmaker, scoped_session
db = SQLAlchemy()
def make_db(config):
engine = create_engine(config['default']['DATABASE_URI'])
session = scoped_session(sessionmaker(bind=engine))
return session

View File

@ -1,26 +1,25 @@
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from atst.database import db
from atst.models.pe_number import PENumber from atst.models.pe_number import PENumber
from .exceptions import NotFoundError from .exceptions import NotFoundError
class PENumbers(object): class PENumbers(object):
def __init__(self, db_session): @classmethod
self.db_session = db_session def get(cls, number):
pe_number = db.session.query(PENumber).get(number)
def get(self, number):
pe_number = self.db_session.query(PENumber).get(number)
if not pe_number: if not pe_number:
raise NotFoundError("pe_number") raise NotFoundError("pe_number")
return pe_number return pe_number
def create_many(self, list_of_pe_numbers): @classmethod
def create_many(cls, list_of_pe_numbers):
stmt = insert(PENumber).values(list_of_pe_numbers) stmt = insert(PENumber).values(list_of_pe_numbers)
do_update = stmt.on_conflict_do_update( do_update = stmt.on_conflict_do_update(
index_elements=["number"], index_elements=["number"], set_=dict(description=stmt.excluded.description)
set_=dict(description=stmt.excluded.description)
) )
self.db_session.execute(do_update) db.session.execute(do_update)
self.db_session.commit() db.session.commit()

View File

@ -1,9 +1,10 @@
import tornado.gen
from sqlalchemy import exists, and_ from sqlalchemy import exists, and_
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from atst.models import Request, RequestStatusEvent from atst.models import Request, RequestStatusEvent
from atst.database import db
from .exceptions import NotFoundError from .exceptions import NotFoundError
@ -28,67 +29,68 @@ def deep_merge(source, destination: dict):
class Requests(object): class Requests(object):
AUTO_APPROVE_THRESHOLD = 1000000 AUTO_APPROVE_THRESHOLD = 1000000
def __init__(self, db_session): @classmethod
self.db_session = db_session def create(cls, creator_id, body):
def create(self, creator_id, body):
request = Request(creator=creator_id, body=body) request = Request(creator=creator_id, body=body)
status_event = RequestStatusEvent(new_status="incomplete") status_event = RequestStatusEvent(new_status="incomplete")
request.status_events.append(status_event) request.status_events.append(status_event)
self.db_session.add(request) db.session.add(request)
self.db_session.commit() db.session.commit()
return request return request
def exists(self, request_id, creator_id): @classmethod
return self.db_session.query( def exists(cls, request_id, creator_id):
return db.session.query(
exists().where( exists().where(
and_(Request.id == request_id, Request.creator == creator_id) and_(Request.id == request_id, Request.creator == creator_id)
) )
).scalar() ).scalar()
def get(self, request_id): @classmethod
def get(cls, request_id):
try: try:
request = self.db_session.query(Request).filter_by(id=request_id).one() request = db.session.query(Request).filter_by(id=request_id).one()
except NoResultFound: except NoResultFound:
raise NotFoundError("request") raise NotFoundError("request")
return request return request
def get_many(self, creator_id=None): @classmethod
def get_many(cls, creator_id=None):
filters = [] filters = []
if creator_id: if creator_id:
filters.append(Request.creator == creator_id) filters.append(Request.creator == creator_id)
requests = ( requests = (
self.db_session.query(Request) db.session.query(Request)
.filter(*filters) .filter(*filters)
.order_by(Request.time_created.desc()) .order_by(Request.time_created.desc())
.all() .all()
) )
return requests return requests
@tornado.gen.coroutine @classmethod
def submit(self, request): def submit(cls, request):
request.status_events.append(RequestStatusEvent(new_status="submitted")) request.status_events.append(RequestStatusEvent(new_status="submitted"))
if Requests.should_auto_approve(request): if Requests.should_auto_approve(request):
request.status_events.append(RequestStatusEvent(new_status="approved")) request.status_events.append(RequestStatusEvent(new_status="approved"))
self.db_session.add(request) db.session.add(request)
self.db_session.commit() db.session.commit()
return request return request
@tornado.gen.coroutine @classmethod
def update(self, request_id, request_delta): def update(cls, request_id, request_delta):
try: try:
# Query for request matching id, acquiring a row-level write lock. # Query for request matching id, acquiring a row-level write lock.
# https://www.postgresql.org/docs/10/static/sql-select.html#SQL-FOR-UPDATE-SHARE # https://www.postgresql.org/docs/10/static/sql-select.html#SQL-FOR-UPDATE-SHARE
request = ( request = (
self.db_session.query(Request) db.session.query(Request)
.filter_by(id=request_id) .filter_by(id=request_id)
.with_for_update(of=Request) .with_for_update(of=Request)
.one() .one()
@ -99,14 +101,16 @@ class Requests(object):
request.body = deep_merge(request_delta, request.body) request.body = deep_merge(request_delta, request.body)
if Requests.should_allow_submission(request): if Requests.should_allow_submission(request):
request.status_events.append(RequestStatusEvent(new_status="pending_submission")) request.status_events.append(
RequestStatusEvent(new_status="pending_submission")
)
# Without this, sqlalchemy won't notice the change to request.body, # Without this, sqlalchemy won't notice the change to request.body,
# since it doesn't track dictionary mutations by default. # since it doesn't track dictionary mutations by default.
flag_modified(request, "body") flag_modified(request, "body")
self.db_session.add(request) db.session.add(request)
self.db_session.commit() db.session.commit()
@classmethod @classmethod
def should_auto_approve(cls, request): def should_auto_approve(cls, request):

View File

@ -5,13 +5,14 @@ from .exceptions import NotFoundError
class TaskOrders(object): class TaskOrders(object):
def __init__(self, db_session): def __init__(self, db_session):
self.db_session = db_session self.db_session = db_session
def get(self, order_number): def get(self, order_number):
try: try:
task_order = self.db_session.query(TaskOrder).filter_by(number=order_number).one() task_order = (
self.db_session.query(TaskOrder).filter_by(number=order_number).one()
)
except NoResultFound: except NoResultFound:
raise NotFoundError("task_order") raise NotFoundError("task_order")

112
atst/eda_client.py Normal file
View File

@ -0,0 +1,112 @@
class EDAClientBase(object):
def list_contracts(
self,
contract_number=None,
delivery_order=None,
cage_code=None,
duns_number=None,
):
"""
Get a list of all contracts matching the given filters.
"""
raise NotImplementedError()
def get_contract(self, contract_number, status):
"""
Get details for a contract.
"""
raise NotImplementedError()
class MockEDAClient(EDAClientBase):
def __init__(self, *args, **kwargs):
pass
def list_contracts(
self,
contract_number=None,
delivery_order=None,
cage_code=None,
duns_number=None,
):
return [
{
"aco_mod": "01",
"admin_dodaac": None,
"cage_code": "1U305",
"contract_no": "DCA10096D0052",
"delivery_order": "0084",
"duns_number": None,
"issue_date": "20000228",
"issue_dodaac": None,
"location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=",
"pay_dodaac": None,
"pco_mod": "02",
},
{
"aco_mod": "01",
"admin_dodaac": None,
"cage_code": "1U305",
"contract_no": "DCA10096D0052",
"delivery_order": "0084",
"duns_number": None,
"issue_date": "20000228",
"issue_dodaac": None,
"location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=",
"pay_dodaac": None,
"pco_mod": "02",
},
{
"aco_mod": "01",
"admin_dodaac": None,
"cage_code": "1U305",
"contract_no": "DCA10096D0052",
"delivery_order": "0084",
"duns_number": None,
"issue_date": "20000228",
"issue_dodaac": None,
"location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=",
"pay_dodaac": None,
"pco_mod": "02",
},
]
def get_contract(self, contract_number, status):
if contract_number == "DCA10096D0052" and status == "y":
return {
"aco_mod": "01",
"admin_dodaac": None,
"cage_code": "1U305",
"contract_no": "DCA10096D0052",
"delivery_order": "0084",
"duns_number": None,
"issue_date": "20000228",
"issue_dodaac": None,
"location": "https://docsrv1.nit.disa.mil:443/eda/enforcer/C0414345.PDF?ver=1.4&loc=Y29udHJhY3RzL29nZGVuL3ZlbmRvci8xOTk4LzA5LzE0L0MwNDE0MzQ1LlBERg==&sourceurl=aHR0cHM6Ly9lZGE0Lm5pdC5kaXNhLm1pbC9wbHMvdXNlci9uZXdfYXBwLkdldF9Eb2M_cFRhYmxlX0lEPTImcFJlY29yZF9LZXk9OEE2ODExNjM2RUY5NkU2M0UwMzQwMDYwQjBCMjgyNkM=&uid=6CFC2B2322E86FD5E054002264936E3C&qid=19344159&signed=G&qdate=20180529194407GMT&token=6xQICrrrfIMciEJSpXmfsAYrToM=",
"pay_dodaac": None,
"pco_mod": "02",
"amount": 2000000,
}
else:
return None
class EDAClient(EDAClientBase):
def __init__(self, base_url, user_name, user_role):
pass
def list_contracts(
self,
contract_number=None,
delivery_order=None,
cage_code=None,
duns_number=None,
):
# TODO: Fetch the contracts CSV and transform them into dictionaries.
# https://docs.python.org/3/library/csv.html#csv.DictReader
raise NotImplementedError()
def get_contract(self, contract_number, status):
# TODO: Fetch the contract XML and transform it into a dictionary.
# https://docs.python.org/3.7/library/xml.etree.elementtree.html
raise NotImplementedError()

View File

@ -7,7 +7,14 @@ import pendulum
class DateField(DateField): class DateField(DateField):
def _value(self): def _value(self):
if self.data: if self.data:
return pendulum.parse(self.data).date() date_formats = ["YYYY-MM-DD", "MM/DD/YYYY"]
for _format in date_formats:
try:
return pendulum.from_format(self.data, _format).date()
except (ValueError, pendulum.parsing.exceptions.ParserError):
pass
raise ValueError("Unable to parse string {}".format(self.data))
else: else:
return None return None

View File

@ -1,24 +1,27 @@
import re import re
import tornado
from tornado.gen import Return
from wtforms.fields.html5 import EmailField from wtforms.fields.html5 import EmailField
from wtforms.fields import StringField, SelectField from wtforms.fields import StringField, SelectField
from wtforms.form import Form from wtforms.form import Form
from wtforms.validators import Required, Email from wtforms.validators import Required, Email
from atst.domain.exceptions import NotFoundError from atst.domain.exceptions import NotFoundError
from atst.domain.pe_numbers import PENumbers
from .fields import NewlineListField from .fields import NewlineListField
from .forms import ValidatedForm from .forms import ValidatedForm
PE_REGEX = re.compile(r""" PE_REGEX = re.compile(
r"""
(0?\d) # program identifier (0?\d) # program identifier
(0?\d) # category (0?\d) # category
(\d) # activity (\d) # activity
(\d+) # sponsor element (\d+) # sponsor element
(.+) # service (.+) # service
""", re.X) """,
re.X,
)
def suggest_pe_id(pe_id): def suggest_pe_id(pe_id):
suggestion = pe_id suggestion = pe_id
@ -36,31 +39,28 @@ def suggest_pe_id(pe_id):
return None return None
@tornado.gen.coroutine def validate_pe_id(field, existing_request):
def validate_pe_id(field, existing_request, pe_numbers_repo):
try: try:
pe_number = pe_numbers_repo.get(field.data) pe_number = PENumbers.get(field.data)
except NotFoundError: except NotFoundError:
suggestion = suggest_pe_id(field.data) suggestion = suggest_pe_id(field.data)
error_str = ( error_str = (
"We couldn't find that PE number. {}" "We couldn't find that PE number. {}"
"If you have double checked it you can submit anyway. " "If you have double checked it you can submit anyway. "
"Your request will need to go through a manual review." "Your request will need to go through a manual review."
).format("Did you mean \"{}\"? ".format(suggestion) if suggestion else "") ).format('Did you mean "{}"? '.format(suggestion) if suggestion else "")
field.errors.append(error_str) field.errors += (error_str,)
return False return False
return True return True
class FinancialForm(ValidatedForm): class FinancialForm(ValidatedForm):
def perform_extra_validation(self, existing_request):
@tornado.gen.coroutine
def perform_extra_validation(self, existing_request, pe_numbers_repo):
valid = True valid = True
if not existing_request or existing_request.get('pe_id') != self.pe_id.data: if not existing_request or existing_request.get("pe_id") != self.pe_id.data:
valid = yield validate_pe_id(self.pe_id, existing_request, pe_numbers_repo) valid = validate_pe_id(self.pe_id, existing_request)
raise Return(valid) return valid
task_order_id = StringField( task_order_id = StringField(
"Task Order Number associated with this request.", validators=[Required()] "Task Order Number associated with this request.", validators=[Required()]
@ -70,9 +70,7 @@ class FinancialForm(ValidatedForm):
"Unique Item Identifier (UII)s related to your application(s) if you already have them." "Unique Item Identifier (UII)s related to your application(s) if you already have them."
) )
pe_id = StringField( pe_id = StringField("Program Element (PE) Number related to your request")
"Program Element (PE) Number related to your request"
)
treasury_code = StringField("Program Treasury Code") treasury_code = StringField("Program Treasury Code")
@ -118,11 +116,13 @@ class FinancialForm(ValidatedForm):
) )
clin_0001 = StringField( clin_0001 = StringField(
"<dl><dt>CLIN 0001</dt> - <dd>Unclassified IaaS and PaaS Amount</dd></dl>", validators=[Required()] "<dl><dt>CLIN 0001</dt> - <dd>Unclassified IaaS and PaaS Amount</dd></dl>",
validators=[Required()],
) )
clin_0003 = StringField( clin_0003 = StringField(
"<dl><dt>CLIN 0003</dt> - <dd>Unclassified Cloud Support Package</dd></dl>", validators=[Required()] "<dl><dt>CLIN 0003</dt> - <dd>Unclassified Cloud Support Package</dd></dl>",
validators=[Required()],
) )
clin_1001 = StringField( clin_1001 = StringField(

View File

@ -1,12 +1,10 @@
import tornado import tornado
from tornado.gen import Return from tornado.gen import Return
from wtforms_tornado import Form from flask_wtf import FlaskForm
class ValidatedForm(Form): class ValidatedForm(FlaskForm):
@tornado.gen.coroutine
def perform_extra_validation(self, *args, **kwargs): def perform_extra_validation(self, *args, **kwargs):
"""A coroutine that performs any applicable extra validation. Must """Performs any applicable extra validation. Must
return True if the form is valid or False otherwise.""" return True if the form is valid or False otherwise."""
raise Return(True) return True

View File

@ -8,30 +8,15 @@ from .validators import DateRange, PhoneNumber, Alphabet
class OrgForm(ValidatedForm): class OrgForm(ValidatedForm):
fname_request = StringField( fname_request = StringField("First Name", validators=[Required(), Alphabet()])
"First Name",
validators=[Required(), Alphabet()]
)
lname_request = StringField( lname_request = StringField("Last Name", validators=[Required(), Alphabet()])
"Last Name",
validators=[Required(), Alphabet()]
)
email_request = EmailField( email_request = EmailField("Email Address", validators=[Required(), Email()])
"Email Address",
validators=[Required(), Email()]
)
phone_number = TelField( phone_number = TelField("Phone Number", validators=[Required(), PhoneNumber()])
"Phone Number",
validators=[Required(), PhoneNumber()]
)
service_branch = StringField( service_branch = StringField("Service Branch or Agency", validators=[Required()])
"Service Branch or Agency",
validators=[Required()]
)
citizenship = RadioField( citizenship = RadioField(
choices=[ choices=[

View File

@ -6,22 +6,12 @@ from .validators import IsNumber, Alphabet
class POCForm(ValidatedForm): class POCForm(ValidatedForm):
fname_poc = StringField( fname_poc = StringField("POC First Name", validators=[Required()])
"POC First Name",
validators=[Required()]
)
lname_poc = StringField( lname_poc = StringField("POC Last Name", validators=[Required()])
"POC Last Name",
validators=[Required()]
)
email_poc = EmailField( email_poc = EmailField("POC Email Address", validators=[Required(), Email()])
"POC Email Address",
validators=[Required(), Email()]
)
dodid_poc = StringField( dodid_poc = StringField(
"DOD ID", "DOD ID", validators=[Required(), Length(min=10), IsNumber()]
validators=[Required(), Length(min=10), IsNumber()]
) )

View File

@ -14,18 +14,21 @@ class RequestForm(ValidatedForm):
"DoD Component", "DoD Component",
description="Identify the DoD component that is requesting access to the JEDI Cloud", description="Identify the DoD component that is requesting access to the JEDI Cloud",
choices=[ choices=[
("null","Select an option"), ("null", "Select an option"),
("us_air_force","US Air Force"), ("us_air_force", "US Air Force"),
("us_army","US Army"), ("us_army", "US Army"),
("us_navy","US Navy"), ("us_navy", "US Navy"),
("us_marine_corps","US Marine Corps"), ("us_marine_corps", "US Marine Corps"),
("joint_chiefs_of_staff","Joint Chiefs of Staff")], ("joint_chiefs_of_staff", "Joint Chiefs of Staff"),
],
) )
jedi_usage = TextAreaField( jedi_usage = TextAreaField(
"JEDI Usage", "JEDI Usage",
description="Briefly describe how you are expecting to use the JEDI Cloud", description="Briefly describe how you are expecting to use the JEDI Cloud",
render_kw={"placeholder": "e.g. We are migrating XYZ application to the cloud so that..."}, render_kw={
"placeholder": "e.g. We are migrating XYZ application to the cloud so that..."
},
) )
# Details of Use: Cloud Readiness # Details of Use: Cloud Readiness
@ -41,7 +44,7 @@ class RequestForm(ValidatedForm):
rationalization_software_systems = RadioField( rationalization_software_systems = RadioField(
"Have you completed a “rationalization” of your software systems to move to the cloud?", "Have you completed a “rationalization” of your software systems to move to the cloud?",
choices=[("yes", "Yes"), ("no", "No"), ("in_progress","In Progress")], choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
) )
technical_support_team = RadioField( technical_support_team = RadioField(
@ -52,40 +55,43 @@ class RequestForm(ValidatedForm):
organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio organization_providing_assistance = RadioField( # this needs to be updated to use checkboxes instead of radio
"If you are receiving migration assistance, indicate the type of organization providing assistance below:", "If you are receiving migration assistance, indicate the type of organization providing assistance below:",
choices=[ choices=[
("in_house_staff","In-house staff"), ("in_house_staff", "In-house staff"),
("contractor","Contractor"), ("contractor", "Contractor"),
("other_dod_organization","Other DoD organization")], ("other_dod_organization", "Other DoD organization"),
],
) )
engineering_assessment = RadioField( engineering_assessment = RadioField(
description="Have you completed an engineering assessment of your software systems for cloud readiness?", description="Have you completed an engineering assessment of your software systems for cloud readiness?",
choices=[("yes", "Yes"), ("no", "No"), ("in_progress","In Progress")], choices=[("yes", "Yes"), ("no", "No"), ("in_progress", "In Progress")],
) )
data_transfers = SelectField( data_transfers = SelectField(
description="How much data is being transferred to the cloud?", description="How much data is being transferred to the cloud?",
choices=[ choices=[
("null","Select an option"), ("null", "Select an option"),
("less_than_100gb","Less than 100GB"), ("less_than_100gb", "Less than 100GB"),
("100gb-500gb","100GB-500GB"), ("100gb-500gb", "100GB-500GB"),
("500gb-1tb","500GB-1TB"), ("500gb-1tb", "500GB-1TB"),
("1tb-50tb","1TB-50TB"), ("1tb-50tb", "1TB-50TB"),
("50tb-100tb","50TB-100TB"), ("50tb-100tb", "50TB-100TB"),
("100tb-500tb","100TB-500TB"), ("100tb-500tb", "100TB-500TB"),
("500tb-1pb","500TB-1PB"), ("500tb-1pb", "500TB-1PB"),
("1pb-5pb","1PB-5PB"), ("1pb-5pb", "1PB-5PB"),
("5pb-10pb","5PB-10PB"), ("5pb-10pb", "5PB-10PB"),
("above_10pb","Above 10PB")], ("above_10pb", "Above 10PB"),
],
) )
expected_completion_date = SelectField( expected_completion_date = SelectField(
description="When do you expect to complete your migration to the JEDI Cloud?", description="When do you expect to complete your migration to the JEDI Cloud?",
choices=[ choices=[
("null","Select an option"), ("null", "Select an option"),
("less_than_1_month","Less than 1 month"), ("less_than_1_month", "Less than 1 month"),
("1_to_3_months","1-3 months"), ("1_to_3_months", "1-3 months"),
("3_to_6_months","3-6 months"), ("3_to_6_months", "3-6 months"),
("above_12_months","Above 12 months")], ("above_12_months", "Above 12 months"),
],
) )
cloud_native = RadioField( cloud_native = RadioField(
@ -96,7 +102,7 @@ class RequestForm(ValidatedForm):
# Details of Use: Financial Usage # Details of Use: Financial Usage
estimated_monthly_spend = IntegerField( estimated_monthly_spend = IntegerField(
"Estimated monthly spend", "Estimated monthly spend",
description="Use the <a href=\"#\">JEDI CSP Calculator</a> to estimate your monthly cloud resource usage and enter the dollar amount below. Note these estimates are for initial approval only. After the request is approved, you will be asked to provide a valid Task Order number with specific CLIN amounts for cloud services." description='Use the <a href="#">JEDI CSP Calculator</a> to estimate your monthly cloud resource usage and enter the dollar amount below. Note these estimates are for initial approval only. After the request is approved, you will be asked to provide a valid Task Order number with specific CLIN amounts for cloud services.',
) )
dollar_value = IntegerField( dollar_value = IntegerField(
@ -105,17 +111,13 @@ class RequestForm(ValidatedForm):
) )
number_user_sessions = IntegerField( number_user_sessions = IntegerField(
description="How many user sessions do you expect on these systems each day?", description="How many user sessions do you expect on these systems each day?"
) )
average_daily_traffic = IntegerField( average_daily_traffic = IntegerField(
description="What is the average daily traffic you expect the systems under this cloud contract to use?", description="What is the average daily traffic you expect the systems under this cloud contract to use?"
) )
start_date = DateField( start_date = DateField(
description="When do you expect to start using the JEDI Cloud (not for billing purposes)?", description="When do you expect to start using the JEDI Cloud (not for billing purposes)?"
) )

View File

@ -1,13 +1,11 @@
import tornado.web import tornado.web
from atst.assets import environment
from atst.sessions import SessionNotFoundError from atst.sessions import SessionNotFoundError
from atst.domain.users import Users from atst.domain.users import Users
helpers = {"assets": environment} helpers = {"assets": None}
class BaseHandler(tornado.web.RequestHandler): class BaseHandler(tornado.web.RequestHandler):
def get_template_namespace(self): def get_template_namespace(self):
ns = super(BaseHandler, self).get_template_namespace() ns = super(BaseHandler, self).get_template_namespace()
helpers["config"] = self.application.config helpers["config"] = self.application.config

View File

@ -8,42 +8,42 @@ _DEV_USERS = {
"id": "164497f6-c1ea-4f42-a5ef-101da278c012", "id": "164497f6-c1ea-4f42-a5ef-101da278c012",
"first_name": "Sam", "first_name": "Sam",
"last_name": "Seeceepio", "last_name": "Seeceepio",
"atat_role": "ccpo" "atat_role": "ccpo",
}, },
"amanda": { "amanda": {
"id": "cce17030-4109-4719-b958-ed109dbb87c8", "id": "cce17030-4109-4719-b958-ed109dbb87c8",
"first_name": "Amanda", "first_name": "Amanda",
"last_name": "Adamson", "last_name": "Adamson",
"atat_role": "default" "atat_role": "default",
}, },
"brandon": { "brandon": {
"id": "66ebf7b8-cbf0-4ed8-a102-5f105330df75", "id": "66ebf7b8-cbf0-4ed8-a102-5f105330df75",
"first_name": "Brandon", "first_name": "Brandon",
"last_name": "Buchannan", "last_name": "Buchannan",
"atat_role": "default" "atat_role": "default",
}, },
"christina": { "christina": {
"id": "7707b9f2-5945-49ae-967a-be65baa88baf", "id": "7707b9f2-5945-49ae-967a-be65baa88baf",
"first_name": "Christina", "first_name": "Christina",
"last_name": "Collins", "last_name": "Collins",
"atat_role": "default" "atat_role": "default",
}, },
"dominick": { "dominick": {
"id": "6978ac0c-442a-46aa-a0c3-ff17b5ec2a8c", "id": "6978ac0c-442a-46aa-a0c3-ff17b5ec2a8c",
"first_name": "Dominick", "first_name": "Dominick",
"last_name": "Domingo", "last_name": "Domingo",
"atat_role": "default" "atat_role": "default",
}, },
"erica": { "erica": {
"id": "596fd001-bb1d-4adf-87d8-fa2312e882de", "id": "596fd001-bb1d-4adf-87d8-fa2312e882de",
"first_name": "Erica", "first_name": "Erica",
"last_name": "Eichner", "last_name": "Eichner",
"atat_role": "default" "atat_role": "default",
}, },
} }
class Dev(BaseHandler):
class Dev(BaseHandler):
def initialize(self, action, sessions, db_session): def initialize(self, action, sessions, db_session):
self.db_session = db_session self.db_session = db_session
self.action = action self.action = action

View File

@ -19,7 +19,7 @@ class RequestFinancialVerification(BaseHandler):
@tornado.gen.coroutine @tornado.gen.coroutine
def get(self, request_id=None): def get(self, request_id=None):
existing_request = self.get_existing_request(request_id) existing_request = self.get_existing_request(request_id)
form = FinancialForm(data=existing_request.body.get('financial_verification')) form = FinancialForm(data=existing_request.body.get("financial_verification"))
self.render( self.render(
"requests/financial_verification.html.to", "requests/financial_verification.html.to",
page=self.page, page=self.page,
@ -47,21 +47,18 @@ class RequestFinancialVerification(BaseHandler):
if form.validate(): if form.validate():
yield self.update_request(request_id, form.data) yield self.update_request(request_id, form.data)
# pylint: disable=E1121
valid = yield form.perform_extra_validation( valid = yield form.perform_extra_validation(
existing_request.body.get('financial_verification'), existing_request.body.get("financial_verification"),
self.pe_numbers_repo self.pe_numbers_repo,
) )
if valid: if valid:
self.redirect( self.redirect(
self.application.default_router.reverse_url("financial_verification_submitted") self.application.default_router.reverse_url(
"financial_verification_submitted"
)
) )
else: else:
self.render( self.render("requests/financial_verification.html.to", **rerender_args)
"requests/financial_verification.html.to",
**rerender_args
)
else: else:
self.render( self.render("requests/financial_verification.html.to", **rerender_args)
"requests/financial_verification.html.to",
**rerender_args
)

View File

@ -58,19 +58,15 @@ class RequestNew(BaseHandler):
where = "/requests" where = "/requests"
else: else:
where = self.application.default_router.reverse_url( where = self.application.default_router.reverse_url(
"request_form_update", jedi_flow.next_screen, jedi_flow.request_id "request_form_update",
jedi_flow.next_screen,
jedi_flow.request_id,
) )
self.redirect(where) self.redirect(where)
else: else:
self.render( self.render("requests/screen-%d.html.to" % int(screen), **rerender_args)
"requests/screen-%d.html.to" % int(screen),
**rerender_args
)
else: else:
self.render( self.render("requests/screen-%d.html.to" % int(screen), **rerender_args)
"requests/screen-%d.html.to" % int(screen),
**rerender_args
)
@tornado.web.authenticated @tornado.web.authenticated
@tornado.gen.coroutine @tornado.gen.coroutine
@ -82,7 +78,11 @@ class RequestNew(BaseHandler):
request = self.requests_repo.get(request_id) request = self.requests_repo.get(request_id)
jedi_flow = JEDIRequestFlow( jedi_flow = JEDIRequestFlow(
self.requests_repo, self.pe_numbers_repo, screen, request, request_id=request_id self.requests_repo,
self.pe_numbers_repo,
screen,
request,
request_id=request_id,
) )
self.render( self.render(
@ -94,14 +94,13 @@ class RequestNew(BaseHandler):
current=screen, current=screen,
next_screen=screen + 1, next_screen=screen + 1,
request_id=request_id, request_id=request_id,
can_submit=jedi_flow.can_submit can_submit=jedi_flow.can_submit,
) )
class JEDIRequestFlow(object): class JEDIRequestFlow(object):
def __init__( def __init__(
self, self,
requests_repo,
pe_numbers_repo, pe_numbers_repo,
current_step, current_step,
request=None, request=None,
@ -110,6 +109,8 @@ class JEDIRequestFlow(object):
current_user=None, current_user=None,
existing_request=None, existing_request=None,
): ):
self.pe_numbers_repo = pe_numbers_repo
self.requests_repo = requests_repo self.requests_repo = requests_repo
self.pe_numbers_repo = pe_numbers_repo self.pe_numbers_repo = pe_numbers_repo
@ -139,13 +140,11 @@ class JEDIRequestFlow(object):
@tornado.gen.coroutine @tornado.gen.coroutine
def validate_warnings(self): def validate_warnings(self):
existing_request_data = ( existing_request_data = (
self.existing_request self.existing_request and self.existing_request.body.get(self.form_section)
and self.existing_request.body.get(self.form_section)
) or None ) or None
valid = yield self.form.perform_extra_validation( valid = yield self.form.perform_extra_validation(
existing_request_data, existing_request_data, self.pe_numbers_repo
self.pe_numbers_repo,
) )
return valid return valid
@ -173,7 +172,7 @@ class JEDIRequestFlow(object):
else: else:
data = self.request.body.get(self.form_section, {}) data = self.request.body.get(self.form_section, {})
return defaultdict(lambda: defaultdict(lambda: 'Input required'), data) return defaultdict(lambda: defaultdict(lambda: "Input required"), data)
@property @property
def can_submit(self): def can_submit(self):
@ -216,14 +215,12 @@ class JEDIRequestFlow(object):
"title": "Review & Submit", "title": "Review & Submit",
"section": "review_submit", "section": "review_submit",
"form": ReviewForm, "form": ReviewForm,
"show":True, "show": True,
}, },
] ]
def create_or_update_request(self): def create_or_update_request(self):
request_data = { request_data = {self.form_section: self.form.data}
self.form_section: self.form.data
}
if self.request_id: if self.request_id:
self.requests_repo.update(self.request_id, request_data) self.requests_repo.update(self.request_id, request_data)
else: else:

View File

@ -12,4 +12,6 @@ class Workspace(BaseHandler):
@tornado.gen.coroutine @tornado.gen.coroutine
def get(self, workspace_id): def get(self, workspace_id):
projects = self.projects_repo.get_many(workspace_id) projects = self.projects_repo.get_many(workspace_id)
self.render("workspace_projects.html.to", workspace_id=workspace_id, projects=projects) self.render(
"workspace_projects.html.to", workspace_id=workspace_id, projects=projects
)

View File

@ -12,4 +12,6 @@ class WorkspaceMembers(BaseHandler):
@tornado.gen.coroutine @tornado.gen.coroutine
def get(self, workspace_id): def get(self, workspace_id):
members = self.members_repo.get_many(workspace_id) members = self.members_repo.get_many(workspace_id)
self.render("workspace_members.html.to", workspace_id=workspace_id, members=members) self.render(
"workspace_members.html.to", workspace_id=workspace_id, members=members
)

View File

@ -2,6 +2,7 @@ from sqlalchemy import String, Column
from atst.models import Base from atst.models import Base
class PENumber(Base): class PENumber(Base):
__tablename__ = "pe_number" __tablename__ = "pe_number"
@ -10,4 +11,5 @@ class PENumber(Base):
def __repr__(self): def __repr__(self):
return "<PENumber(number='{}', description='{}')>".format( return "<PENumber(number='{}', description='{}')>".format(
self.number, self.description) self.number, self.description
)

View File

@ -8,15 +8,15 @@ from atst.models.types import Id
class Request(Base): class Request(Base):
__tablename__ = 'requests' __tablename__ = "requests"
id = Id() id = Id()
creator = Column(UUID(as_uuid=True)) creator = Column(UUID(as_uuid=True))
time_created = Column(DateTime(timezone=True), server_default=func.now()) time_created = Column(DateTime(timezone=True), server_default=func.now())
body = Column(JSONB) body = Column(JSONB)
status_events = relationship('RequestStatusEvent', status_events = relationship(
backref='request', "RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence"
order_by='RequestStatusEvent.sequence') )
@property @property
def status(self): def status(self):

View File

@ -8,10 +8,14 @@ from atst.models.types import Id
class RequestStatusEvent(Base): class RequestStatusEvent(Base):
__tablename__ = 'request_status_events' __tablename__ = "request_status_events"
id = Id() id = Id()
new_status = Column(String()) new_status = Column(String())
time_created = Column(DateTime(timezone=True), server_default=func.now()) time_created = Column(DateTime(timezone=True), server_default=func.now())
request_id = Column(UUID(as_uuid=True), ForeignKey('requests.id', ondelete='CASCADE')) request_id = Column(
sequence = Column(BigInteger, Sequence('request_status_events_sequence_seq'), nullable=False) UUID(as_uuid=True), ForeignKey("requests.id", ondelete="CASCADE")
)
sequence = Column(
BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False
)

View File

@ -2,6 +2,7 @@ from sqlalchemy import Column, Integer, String
from atst.models import Base from atst.models import Base
class TaskOrder(Base): class TaskOrder(Base):
__tablename__ = "task_order" __tablename__ = "task_order"

View File

@ -7,4 +7,5 @@ def Id():
return Column( return Column(
UUID(as_uuid=True), UUID(as_uuid=True),
primary_key=True, primary_key=True,
server_default=sqlalchemy.text("uuid_generate_v4()")) server_default=sqlalchemy.text("uuid_generate_v4()"),
)

26
atst/routes/__init__.py Normal file
View File

@ -0,0 +1,26 @@
from flask import Blueprint, render_template, g
import pendulum
from atst.domain.requests import Requests
bp = Blueprint("atst", __name__)
@bp.route("/")
def root():
return render_template("root.html")
@bp.route("/home")
def home():
return render_template("home.html")
@bp.route("/styleguide")
def styleguide():
return render_template("styleguide.html")
@bp.route('/<path:path>')
def catch_all(path):
return render_template("{}.html".format(path))

View File

@ -0,0 +1,7 @@
from flask import Blueprint
requests_bp = Blueprint("requests", __name__)
from . import index
from . import requests_form
from . import financial_verification

View File

@ -0,0 +1,44 @@
from flask import render_template, redirect, url_for
from flask import request as http_request
from . import requests_bp
from atst.domain.requests import Requests
from atst.forms.financial import FinancialForm
@requests_bp.route("/requests/verify/<string:request_id>", methods=["GET"])
def financial_verification(request_id=None):
request = Requests.get(request_id)
form = FinancialForm(data=request.body.get("financial_verification"))
return render_template(
"requests/financial_verification.html", f=form, request_id=request_id
)
@requests_bp.route("/requests/verify/<string:request_id>", methods=["POST"])
def update_financial_verification(request_id):
post_data = http_request.form
existing_request = Requests.get(request_id)
form = FinancialForm(post_data)
rerender_args = dict(request_id=request_id, f=form)
if form.validate():
request_data = {"financial_verification": post_data}
Requests.update(request_id, request_data)
valid = form.perform_extra_validation(
existing_request.body.get("financial_verification")
)
if valid:
return redirect(url_for("requests.financial_verification_submitted"))
else:
return render_template(
"requests/financial_verification.html", **rerender_args
)
else:
return render_template("requests/financial_verification.html", **rerender_args)
@requests_bp.route("/requests/financial_verification_submitted")
def financial_verification_submitted():
pass

View File

@ -0,0 +1,35 @@
import pendulum
from flask import render_template, g
from . import requests_bp
from atst.domain.requests import Requests
def map_request(user, request):
time_created = pendulum.instance(request.time_created)
is_new = time_created.add(days=1) > pendulum.now()
return {
"order_id": request.id,
"is_new": is_new,
"status": request.status,
"app_count": 1,
"date": time_created.format("M/DD/YYYY"),
"full_name": "{} {}".format(user["first_name"], user["last_name"]),
}
@requests_bp.route("/requests", methods=["GET"])
def requests_index():
requests = []
if (
"review_and_approve_jedi_workspace_request"
in g.current_user["atat_permissions"]
):
requests = Requests.get_many()
else:
requests = Requests.get_many(creator_id=g.current_user["id"])
mapped_requests = [map_request(g.current_user, r) for r in requests]
return render_template("requests.html", requests=mapped_requests)

View File

@ -0,0 +1,128 @@
from collections import defaultdict
from atst.domain.requests import Requests
from atst.forms.request import RequestForm
from atst.forms.org import OrgForm
from atst.forms.poc import POCForm
from atst.forms.review import ReviewForm
class JEDIRequestFlow(object):
def __init__(
self,
current_step,
request=None,
post_data=None,
request_id=None,
current_user=None,
existing_request=None,
):
self.current_step = current_step
self.request = request
self.post_data = post_data
self.is_post = self.post_data is not None
self.request_id = request_id
self.form = self._form()
self.current_user = current_user
self.existing_request = existing_request
def _form(self):
if self.is_post:
return self.form_class()(self.post_data)
elif self.request:
return self.form_class()(data=self.current_step_data)
else:
return self.form_class()()
def validate(self):
return self.form.validate()
def validate_warnings(self):
existing_request_data = (
self.existing_request and self.existing_request.body.get(self.form_section)
) or None
valid = self.form.perform_extra_validation(existing_request_data)
return valid
@property
def current_screen(self):
return self.screens[self.current_step - 1]
@property
def form_section(self):
return self.current_screen["section"]
def form_class(self):
return self.current_screen["form"]
@property
def current_step_data(self):
data = {}
if self.is_post:
data = self.post_data
if self.request:
if self.form_section == "review_submit":
data = self.request.body
else:
data = self.request.body.get(self.form_section, {})
return defaultdict(lambda: defaultdict(lambda: "Input required"), data)
@property
def can_submit(self):
return self.request and self.request.status != "incomplete"
@property
def next_screen(self):
return self.current_step + 1
@property
def screens(self):
return [
{
"title": "Details of Use",
"section": "details_of_use",
"form": RequestForm,
"subitems": [
{
"title": "Overall request details",
"id": "overall-request-details",
},
{"title": "Cloud Resources", "id": "cloud-resources"},
{"title": "Support Staff", "id": "support-staff"},
],
"show": True,
},
{
"title": "Information About You",
"section": "information_about_you",
"form": OrgForm,
"show": True,
},
{
"title": "Primary Point of Contact",
"section": "primary_poc",
"form": POCForm,
"show": True,
},
{
"title": "Review & Submit",
"section": "review_submit",
"form": ReviewForm,
"show": True,
},
]
def create_or_update_request(self):
request_data = {self.form_section: self.form.data}
if self.request_id:
Requests.update(self.request_id, request_data)
else:
request = Requests.create(self.current_user["id"], request_data)
self.request_id = request.id

View File

@ -0,0 +1,99 @@
from flask import g, redirect, render_template, url_for, request as http_request
from . import requests_bp
from atst.domain.requests import Requests
from atst.routes.requests.jedi_request_flow import JEDIRequestFlow
@requests_bp.route("/requests/new", defaults={"screen": 1})
@requests_bp.route("/requests/new/<int:screen>", methods=["GET"])
def requests_form_new(screen):
jedi_flow = JEDIRequestFlow(screen, request=None)
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
data=jedi_flow.current_step_data,
screens=jedi_flow.screens,
current=screen,
next_screen=screen + 1,
can_submit=jedi_flow.can_submit,
)
@requests_bp.route(
"/requests/new/<int:screen>", methods=["GET"], defaults={"request_id": None}
)
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["GET"])
def requests_form_update(screen=1, request_id=None):
request = Requests.get(request_id) if request_id is not None else None
jedi_flow = JEDIRequestFlow(screen, request, request_id=request_id)
return render_template(
"requests/screen-%d.html" % int(screen),
f=jedi_flow.form,
data=jedi_flow.current_step_data,
screens=jedi_flow.screens,
current=screen,
next_screen=screen + 1,
request_id=request_id,
can_submit=jedi_flow.can_submit,
)
@requests_bp.route(
"/requests/new/<int:screen>", methods=["POST"], defaults={"request_id": None}
)
@requests_bp.route("/requests/new/<int:screen>/<string:request_id>", methods=["POST"])
def requests_update(screen=1, request_id=None):
screen = int(screen)
post_data = http_request.form
current_user = g.current_user
existing_request = Requests.get(request_id) if request_id is not None else None
jedi_flow = JEDIRequestFlow(
screen,
post_data=post_data,
request_id=request_id,
current_user=current_user,
existing_request=existing_request,
)
rerender_args = dict(
f=jedi_flow.form,
data=post_data,
screens=jedi_flow.screens,
current=screen,
next_screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
if jedi_flow.validate():
jedi_flow.create_or_update_request()
valid = jedi_flow.validate_warnings()
if valid:
if jedi_flow.next_screen > len(jedi_flow.screens):
where = "/requests"
else:
where = url_for(
"requests.requests_form_update",
screen=jedi_flow.next_screen,
request_id=jedi_flow.request_id,
)
return redirect(where)
else:
return render_template(
"requests/screen-%d.html" % int(screen), **rerender_args
)
else:
return render_template("requests/screen-%d.html" % int(screen), **rerender_args)
@requests_bp.route("/requests/submit/<string:request_id>", methods=["POST"])
def requests_submit(request_id=None):
request = Requests.get(request_id)
Requests.submit(request)
if request.status == "approved":
return redirect("/requests?modal=True")
else:
return redirect("/requests")

38
atst/routes/workspaces.py Normal file
View File

@ -0,0 +1,38 @@
from flask import Blueprint, render_template
from atst.domain.workspaces import Projects, Members
bp = Blueprint("workspaces", __name__)
mock_workspaces = [
{
"name": "Unclassified IaaS and PaaS for Defense Digital Service (DDS)",
"id": "5966187a-eff9-44c3-aa15-4de7a65ac7ff",
"task_order": {"number": 123456},
"user_count": 23,
}
]
@bp.route("/workspaces")
def workspaces():
return render_template("workspaces.html", page=5, workspaces=mock_workspaces)
@bp.route("/workspaces/<workspace_id>/projects")
def workspace_projects(workspace_id):
projects_repo = Projects()
projects = projects_repo.get_many(workspace_id)
return render_template(
"workspace_projects.html", workspace_id=workspace_id, projects=projects
)
@bp.route("/workspaces/<workspace_id>/members")
def workspace_members(workspace_id):
members_repo = Members()
members = members_repo.get_many(workspace_id)
return render_template(
"workspace_members.html", workspace_id=workspace_id, members=members
)

View File

@ -1,19 +1,26 @@
import os import os
import re import re
def navigationContext(self): def navigationContext(self):
return 'workspace' if re.match('\/workspaces\/[A-Za-z0-9]*', self.request.uri) else 'global' return (
"workspace"
if re.match("\/workspaces\/[A-Za-z0-9]*", self.request.uri)
else "global"
)
def dev(self): def dev(self):
return os.getenv("TORNADO_ENV", "dev") == "dev" return os.getenv("FLASK_ENV", "dev") == "dev"
def matchesPath(self, href): def matchesPath(self, href):
return re.match('^'+href, self.request.uri) return re.match("^" + href, self.request.uri)
def modal(self, body): def modal(self, body):
return self.render_string( return self.render_string("components/modal.html.to", body=body)
"components/modal.html.to",
body=body)
def modalOpen(self): def modalOpen(self):
# For now, just check a dummy URL param # For now, just check a dummy URL param

View File

@ -1,48 +1,70 @@
from tornado.web import UIModule from tornado.web import UIModule
# from tornado.template import raw # from tornado.template import raw
import re import re
class Alert(UIModule): class Alert(UIModule):
def render(self, title, message=None, actions=None, level='info'): def render(self, title, message=None, actions=None, level="info"):
return self.render_string( return self.render_string(
"components/alert.html.to", "components/alert.html.to",
title=title, title=title,
message=message, message=message,
actions=actions, actions=actions,
level=level) level=level,
)
class TextInput(UIModule): class TextInput(UIModule):
def render(self, field, placeholder=''): def render(self, field, placeholder=""):
return self.render_string( return self.render_string(
"components/text_input.html.to", "components/text_input.html.to",
field=field, field=field,
label=re.sub('<[^<]+?>', '', str(field.label)), label=re.sub("<[^<]+?>", "", str(field.label)),
errors=field.errors, errors=field.errors,
placeholder=placeholder, placeholder=placeholder,
description=field.description) description=field.description,
)
class OptionsInput(UIModule): class OptionsInput(UIModule):
def render(self, field, inline=False): def render(self, field, inline=False):
return self.render_string( return self.render_string(
"components/options_input.html.to", "components/options_input.html.to",
field=field, field=field,
label=re.sub('<[^<]+?>', '', str(field.label)), label=re.sub("<[^<]+?>", "", str(field.label)),
errors=field.errors, errors=field.errors,
description=field.description, description=field.description,
inline=inline) inline=inline,
)
class Icon(UIModule): class Icon(UIModule):
def render(self, name, classes=''): def render(self, name, classes=""):
with open('static/icons/%s.svg' % name) as svg: with open("static/icons/%s.svg" % name) as svg:
return self.render_string( return self.render_string(
"components/icon.html.to", svg=svg.read(), name=name, classes=classes) "components/icon.html.to", svg=svg.read(), name=name, classes=classes
)
class SidenavItem(UIModule): class SidenavItem(UIModule):
def render(self, label, href, active=False, icon=None, subnav=None): def render(self, label, href, active=False, icon=None, subnav=None):
return self.render_string( return self.render_string(
"navigation/_sidenav_item.html.to", "navigation/_sidenav_item.html.to",
label=label, label=label,
href=href, href=href,
active=active, active=active,
icon=icon, icon=icon,
subnav=subnav) subnav=subnav,
)
class EmptyState(UIModule):
def render(self, message, actionLabel, actionHref, icon=None):
return self.render_string(
"components/empty_state.html.to",
message=message,
actionLabel=actionLabel,
actionHref=actionHref,
icon=icon,
)

View File

@ -5,6 +5,7 @@ DEBUG = true
AUTHNID_BASE_URL= https://localhost:8001 AUTHNID_BASE_URL= https://localhost:8001
COOKIE_SECRET = some-secret-please-replace COOKIE_SECRET = some-secret-please-replace
SECRET = change_me_into_something_secret SECRET = change_me_into_something_secret
SECRET_KEY = change_me_into_something_secret
CAC_URL = https://localhost:8001 CAC_URL = https://localhost:8001
PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv PE_NUMBER_CSV_URL = http://c95e1ebb198426ee57b8-174bb05a294821bedbf46b6384fe9b1f.r31.cf5.rackcdn.com/penumbers.csv
REDIS_URI = redis://localhost:6379 REDIS_URI = redis://localhost:6379

2
config/test.ini Normal file
View File

@ -0,0 +1,2 @@
[default]
PGDATABASE = atat_test

View File

@ -4,6 +4,8 @@
source "$(dirname "${0}")"/../script/include/global_header.inc.sh source "$(dirname "${0}")"/../script/include/global_header.inc.sh
export FLASK_ENV=test
# Define all relevant python files and directories for this app # Define all relevant python files and directories for this app
PYTHON_FILES="./app.py ./atst ./config" PYTHON_FILES="./app.py ./atst ./config"

View File

@ -33,3 +33,4 @@
@import 'sections/request_approval'; @import 'sections/request_approval';
@import 'sections/projects_list'; @import 'sections/projects_list';
@import 'sections/project_edit'; @import 'sections/project_edit';
@import 'sections/member_edit';

View File

@ -1,15 +1,27 @@
.empty-state { .empty-state {
text-align: center; text-align: center;
padding-top: 10rem; padding: 6rem ($gap * 2) 0;
display: flex;
p { flex-direction: column;
font-family: $font-sans; align-items: center;
font-weight: 300;
line-height: 10rem; @include media($medium-screen) {
font-size: 44px; padding: 8rem ($gap * 4) 0;
line-height: 5rem;
margin: 0 auto;
padding-bottom: 2rem;
max-width: 40%;
} }
}
.icon {
@include icon-size(50);
@include icon-color($color-gray-light);
}
p {
@include h2;
line-height: 1.2;
max-width: 15em;
color: $color-gray;
@include media($large-screen) {
@include h1;
}
}
}

View File

@ -54,14 +54,26 @@
.block-list { .block-list {
@include block-list; @include block-list;
.icon-link {
margin: -$gap 0;
}
.icon-link,
.label {
&:first-child {
margin-left: -$gap;
}
&:last-child {
margin-right: -$gap;
}
}
} }
.block-list__header { .block-list__header {
@include block-list-header; @include block-list-header;
.block-list__header__link {
@include icon-link;
}
} }
.block-list__title { .block-list__title {

View File

@ -59,4 +59,8 @@
&.icon-link--large { &.icon-link--large {
@include icon-link-large; @include icon-link-large;
} }
&.icon-link--danger {
@include icon-link-color($color-red, $color-red-lightest);
}
} }

View File

@ -159,6 +159,7 @@
select { select {
border-radius: 0; border-radius: 0;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none;
} }
.usa-date-input label { .usa-date-input label {

View File

@ -4,7 +4,7 @@
* @source https://github.com/uswds/uswds/blob/develop/src/stylesheets/elements/_typography.scss * @source https://github.com/uswds/uswds/blob/develop/src/stylesheets/elements/_typography.scss
*/ */
* { * {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
@ -56,8 +56,4 @@ dl {
> div { > div {
margin-bottom: $gap * 2; margin-bottom: $gap * 2;
} }
} }
.subtitle {
color: $color-gray;
}

View File

@ -0,0 +1,41 @@
.member-card {
@include grid-row;
padding: $gap*2;
justify-content: space-between;
dl {
margin: 0;
> div {
margin-bottom: $gap;
}
}
dt {
font-weight: normal;
color: $color-gray;
}
dd {
display: inline;
}
.member-card__header {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.member-card__heading {
margin: 0;
@include h2;
}
.member-card__details {
text-align: right;
.icon-link {
margin: 0 -$gap;
}
}
}

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path fill="#000" d="M8 15L2 7h12z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 147 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path fill="#000" d="M14 9l-8 6V3z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 146 B

38
templates/base.html Normal file
View File

@ -0,0 +1,38 @@
{# TODO: set this context elsewhere #}
{# set context='workspace' #}
{% set context=g.navigationContext %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}JEDI{% endblock %}</title>
{% assets "css" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% endassets %}
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
</head>
<body class="{% if g.modalOpen %} modalOpen{% endif %}">
{% block template_vars %}{% endblock %}
{% include 'navigation/topbar.html' %}
<div class='global-layout'>
{% include 'navigation/global_navigation.html' %}
<div class='global-panel-container'>
{% block sidenav %}{% endblock %}
{% block content %}
these are not the droids you are looking for
{% endblock %}
</div>
</div>
{% include 'footer.html' %}
{% block modal %}{% endblock %}
</body>
</html>

View File

@ -1,40 +0,0 @@
{# TODO: set this context elsewhere #}
{# set context='workspace' #}
{% set context=navigationContext() %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}JEDI{% end %}</title>
{% for url in assets['css'].urls() %}
<link rel="stylesheet" href="{{ url }}" type="text/css">
{% end %}
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
</head>
<body class="{% if modalOpen() %} modalOpen{% end %}">
{% block template_vars %}{% end %}
{% include 'navigation/topbar.html.to' %}
<div class='global-layout'>
{% include 'navigation/global_navigation.html.to' %}
<div class='global-panel-container'>
{% block sidenav %}{% end %}
{% block content %}
these are not the droids you are looking for
{% end %}
</div>
</div>
{% include 'footer.html.to' %}
{% block modal %}{% end %}
</body>
</html>

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %} {% extends "base.html" %}
{% block content %} {% block content %}
@ -8,8 +8,8 @@
</div> </div>
<div class='col col--grow'> <div class='col col--grow'>
{% block workspace_content %}{% end %} {% block workspace_content %}{% endblock %}
</div> </div>
</div> </div>
{% end %} {% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %} {% extends "base.html" %}
{% block content %} {% block content %}
@ -8,5 +8,5 @@
</main> </main>
{% end %} {% endblock %}

145
templates/components.html Normal file
View File

@ -0,0 +1,145 @@
{% macro Icon(name, classes="") -%}
{% autoescape false %}
<span class="icon icon--{{name}} {{classes}}" aria-hidden="true">{{ svg }}</span>
{% endautoescape %}
{%- endmacro %}
{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
<li>
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
{% if icon %}
{{ Icon(icon, classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label">{{label}}</span>
</a>
{% if subnav and active %}
<ul>
{% for item in subnav %}
<li>
<a class="sidenav__link {% if item["active"] %}sidenav__link--active{% endif %}" href="{{item["href"]}}" title="{{item["label"]}}">
{% if "icon" in item %}
{{ Icon(item["icon"], classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label">{{item["label"]}}</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{%- endmacro %}
{% macro Modal() -%}
<div class='modal'>
<div class='modal__dialog' role='dialog' aria-modal='true'>
<div class='modal__body'>
{{ caller() }}
</div>
</div>
</div>
{%- endmacro %}
{% macro EmptyState(message, actionLabel, actionHref, icon=None) -%}
<div class='empty-state'>
<p>{{ message }}</p>
{% if icon %}
{{ Icon(icon) }}
{% endif %}
<a href='{{ actionHref }}' class='usa-button usa-button-big'>{{ actionLabel }}</a>
</div>
{%- endmacro %}
{% macro Alert(title, message=None, actions=None, level='info') -%}
{% set role = 'alertdialog' if actions else 'alert' %}
{% set levels = {
'warning': {
'icon': 'alert',
'tone': 'assertive'
},
'error': {
'icon': 'alert',
'tone': 'assertive'
},
'info': {
'icon': 'info',
'tone': 'polite'
},
'success': {
'icon': 'ok',
'tone': 'polite'
}
} %}
<div class='alert alert--{{level}}' role='{{role}}' aria-live='{{levels.get(level).get('tone')}}'>
{{ Icon(levels.get(level).get('icon'), classes='alert__icon icon--large') }}
<div class='alert__content'>
<h2 class='alert__title'>{{title}}</h2>
{% if message %}
<div class='alert__message'>{{ message | safe }}</div>
{% endif %}
{% if actions %}
<div class='alert__actions'>{{ actions | safe }}</div>
{% endif %}
</div>
</div>
{%- endmacro %}
{% macro TextInput(field, placeholder='') -%}
<div class='usa-input {% if errors %}usa-input--error{% endif %}'>
<label for={{field.name}}>
{{ field.label }}
{% if field.description %}
<span class='usa-input__help'>{{ field.description | safe }}</span>
{% endif %}
{% if errors %}
{{ Icon('alert') }}
{% endif %}
</label>
{{ field(placeholder=placeholder) | safe }}
{% if field.errors %}
{% for error in field.errors %}
<span class='usa-input__message'>{{ error }}</span>
{% endfor %}
{% endif %}
</div>
{%- endmacro %}
{% macro OptionsInput(field, inline=False) -%}
<div class='usa-input {% if errors %}usa-input--error{% endif %}'>
<fieldset class="usa-input__choices {% if inline %}usa-input__choices--inline{% endif %}">
<legend>
{{ field.label }}
{% if field.description %}
<span class='usa-input__help'>{{ field.description | safe }}</span>
{% endif %}
{% if field.errors %}
{{ Icon('alert') }}
{% endif %}
</legend>
{{ field() }}
{% if field.errors %}
{% for error in field.errors %}
<span class='usa-input__message'>{{ error }}</span>
{% endfor %}
{% endif %}
</fieldset>
</div>
{%- endmacro %}

View File

@ -0,0 +1,9 @@
<div class='empty-state'>
<p>{{ message }}</p>
{% if icon %}
{% module Icon(icon) %}
{% end %}
<a href='{{ actionHref }}' class='usa-button usa-button-big'>{{ actionLabel }}</a>
</div>

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %} {% extends "base.html" %}
{% block content %} {% block content %}
@ -8,7 +8,7 @@
</main> </main>
{% end %} {% endblock %}

View File

@ -0,0 +1,85 @@
{% extends "base_workspace.html.to" %}
{% block template_vars %}
{% set is_new_member = False %}
{% set member_name = "Danny Knight" %}
{% set member_email = "knight@mil.gov" %}
{% set member_workspace_role = "Billing Auditor" %}
{% set member_id = "789" %}
{% end %}
{% block workspace_content %}
{% module Alert(
"UI Mock",
message="<p>Please note, this screen is a non-functional UI mockup.</p>",
level="info"
) %}
<div class='panel'>
<div class='panel__heading'>
<h1 class='h2'>
{% if is_new_member %}
Add new member
{% else %}
{{ member_name }}
{% end %}
</h1>
<div>Workspace Role</span> <span class="label">{{member_workspace_role}}</div>
</div>
</div>
<div class='panel panel__actions'>
<div class='row'>
<div class='col col--grow'>
<form class="usa-search usa-search-small">
<label class="usa-sr-only" for="search-field-small">Search small</label>
<input id="search-field-small" type="search" name="search" placeholder="Search by project name">
<button type="submit">
<span class="usa-sr-only">Search</span>
</button>
</form>
</div>
</div>
</div>
<div class='block-list project-list-item'>
<header class='block-list__header'>
<h2 class='block-list__title'>Code.mil</h2>
<a class="block-list__header__link icon-link icon-link--danger">revoke all access</a>
</header>
<ul>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Development
</span>
<div class='project-list-item__environment__actions'>
<span>no access</span><a href="#" class="icon-link">set role</a>
</div>
</li>
<li class='block-list__item project-list-item__environment'>
<span class='project-list-item__environment'>
Production
</span>
<div class='project-list-item__environment__actions'>
<span>Billing</span><a href="#" class="icon-link">set role</a>
</div>
</li>
</ul>
</div>
<div class='action-group'>
<a href='#' class='action-group__action usa-button usa-button-big'>
{% if is_new_member %}Create{% else %}Save{% end %}
</a>
<a href='#' class='action-group__action icon-link'>
{% module Icon('x') %}
<span>Cancel</span>
</a>
</div>
{% end %}

View File

@ -0,0 +1,25 @@
{% from "components.html" import SidenavItem %}
<div class="global-navigation sidenav global-navigation__context--{{context}}">
<ul>
{% if g.dev %}
{{ SidenavItem("Styleguide",
href="/styleguide",
icon="visible",
active=g.matchesPath('/styleguide'),
subnav=[
{"label":"Subnav 1", "href":"/styleguide?subnav1", "icon": "plus", "active": g.matchesPath('/styleguide?subnav1')},
{"label":"Subnav 2", "href":"/styleguide?subnav2", "active": g.matchesPath('/styleguide?subnav2')},
]) }}
{% endif %}
{{ SidenavItem("Requests",
href="/requests",
icon="document",
active=g.matchesPath('/requests'),
subnav=[
{"label":"New Request", "href":"/requests/new", "icon": "plus", "active": g.matchesPath('/requests/new')},
]
) }}
{{ SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=g.matchesPath('/workspaces')) }}
</ul>
</div>

View File

@ -1,24 +0,0 @@
<div class="global-navigation sidenav global-navigation__context--{{context}}">
<ul>
{% if dev() %}
{% module SidenavItem("Styleguide",
href="/styleguide",
icon="visible",
active=matchesPath('/styleguide'),
subnav=[
{"label":"Subnav 1", "href":"/styleguide?subnav1", "icon": "plus", "active": matchesPath('/styleguide?subnav1')},
{"label":"Subnav 2", "href":"/styleguide?subnav2", "active": matchesPath('/styleguide?subnav2')},
]) %}
{% end %}
{% module SidenavItem("Requests",
href="/requests",
icon="document",
active=matchesPath('/requests'),
subnav=[
{"label":"New Request", "href":"/requests/new", "icon": "plus", "active": matchesPath('/requests/new')},
]
) %}
{% module SidenavItem("Workspaces", href="/workspaces", icon="cloud", active=matchesPath('/workspaces')) %}
</ul>
</div>

View File

@ -1,18 +1,19 @@
{% from "components.html" import Icon %}
<header class="topbar"> <header class="topbar">
<nav class="topbar__navigation"> <nav class="topbar__navigation">
<a href="/home" class="topbar__link topbar__link--shield" title="JEDI Home"> <a href="/home" class="topbar__link topbar__link--shield" title="JEDI Home">
{% module 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 topbar__context--{{context}}">
<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 123456" if context == 'workspace' else "JEDI" }}</span>
{% module Icon('caret_down', classes='topbar__link-icon icon--tiny') %} {{ Icon('caret_down', classes='topbar__link-icon icon--tiny') }}
</a> </a>
<a href="/" class="topbar__link"> <a href="/" class="topbar__link">
<span class="topbar__link-label">{{ current_user["first_name"] + " " + current_user["last_name"] }}</span> <span class="topbar__link-label">{{ g.current_user.first_name + " " + g.current_user.last_name }}</span>
{% module Icon('avatar', classes='topbar__link-icon') %} {{ Icon('avatar', classes='topbar__link-icon') }}
</a> </a>
</div> </div>
</nav> </nav>

View File

@ -17,7 +17,20 @@
{% module SidenavItem( {% module SidenavItem(
"Members", "Members",
href=reverse_url('workspace_members', '123456'), href=reverse_url('workspace_members', '123456'),
active=matchesPath('\/workspaces\/[A-Za-z0-9]*\/members') active=matchesPath('\/workspaces\/[A-Za-z0-9]*\/members'),
subnav=[
{
"label": "Add New Member",
"href": "",
"active": matchesPath('/workspaces/members/new'),
"icon": "plus"
},
{
"label": "Editing Member",
"href": "",
"active": matchesPath('/workspaces/123456/members/789/edit')
}
]
)%} )%}
{% module SidenavItem( {% module SidenavItem(

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %} {% extends "base.html" %}
{% block content %} {% block content %}
@ -8,5 +8,5 @@
</main> </main>
{% end %} {% endblock %}

111
templates/requests.html Normal file
View File

@ -0,0 +1,111 @@
{% extends "base.html" %}
{% from "components.html" import Modal, Alert, EmptyState %}
{% block modal %}
{% if g.modalOpen %}
{% call Modal() %}
<h1>Your request is now approved!</h1>
<p>
Your next step is to create a <b>Task Order (T.O.)</b> associated with
JEDI Cloud. Please consult a <b>Contracting Officer (KO)</b> or
<b>Contracting Officer Representative (COR)</b> to help with this step.
</p>
<p>
Once the Task Order (T.O.) has been created, we will need the following
details to create your account. These details will help keep your cloud
usage in sync with your budget.
</p>
{{ Alert("You'll need these details: ",
message="<p>Task Order Number</p><p>Contracting Officer: Name, E-mail and Office</p>"
) }}
<div class='action-group'>
<a href='/requests' class='action-group__action usa-button'>Close</a>
</div>
{% endcall %}
{% endif %}
{% endblock %}
{% block content %}
{% if not requests %}
{{ EmptyState(
'There are currently no active requests for you to see.',
actionLabel='Create a new JEDI Cloud Request',
actionHref='/requests/new',
icon='document'
) }}
{% else %}
{{ Alert('Pending Financial Verification',
message="<p>Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.</p>"
) }}
<div class="col col--grow">
<form class='search-bar'>
<div class='usa-input search-input'>
<label for='requests-search'>Search requests by Order ID</label>
<input type='search' id='requests-search' name='requests-search' placeholder="Search by Order ID"/>
<button type="submit">
<span class="hide">Search</span>
</button>
</div>
<div class='usa-input'>
<label for='filter-status'>Filter requests by status</label>
<select id="filter-status" name="filter-status">
<option value="" selected disabled>Filter by status</option>
<option value="">Active</option>
<option value="">Pending</option>
<option value="">Denied</option>
</select>
</div>
</form>
<div class='responsive-table-wrapper'>
<table>
<thead>
<tr>
<th scope="col">Order ID</th>
<th scope="col">Request Date</th>
<th scope="col">Requester</th>
<th scope="col">Total Apps</th>
<th scope="col">Status</th>
<th scope="col" class="table-cell--shrink">Actions</th>
</tr>
</thead>
<tbody>
{% for r in requests %}
<tr>
<th scope="row">
<a class='icon-link icon-link--large' href="{{ url_for('requests.requests_form_update', screen=1, request_id=r['order_id']) if r["status"] != "approved" else url_for('requests.financial_verification', request_id=r['order_id']) }}">{{ r['order_id'] }}</a>
{% if r['is_new'] %}<span class="usa-label">New</span>
</th>
{% endif %}
<td>{{ r['date'] }}</td>
<td>{{ r['full_name'] }}</td>
<td>{{ r['app_count'] }}</td>
<td>{{ r['status'] }}</td>
<td class="table-cell--shrink">
<a href="" class='icon-link'>Download</a>
<a href="/request/approval" class='icon-link'>Approval</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -1,97 +0,0 @@
{% extends "base.html.to" %}
{% block modal %}
{% if modalOpen() %}
{% apply modal %}
<h1>Your request is now approved!</h1>
<p>
Your next step is to create a <b>Task Order (T.O.)</b> associated with
JEDI Cloud. Please consult a <b>Contracting Officer (KO)</b> or
<b>Contracting Officer Representative (COR)</b> to help with this step.
</p>
<p>
Once the Task Order (T.O.) has been created, we will need the following
details to create your account. These details will help keep your cloud
usage in sync with your budget.
</p>
{% module Alert("You'll need these details: ",
message="<p>Task Order Number</p><p>Contracting Officer: Name, E-mail and Office</p>"
) %}
<div class='action-group'>
<a href='/requests' class='action-group__action usa-button'>Close</a>
</div>
{% end %}
{% end %}
{% end %}
{% block content %}
{% module Alert('Pending Financial Verification',
message="<p>Your next step is to create a Task Order (T.O.) associated with JEDI Cloud. Please consult a Contracting Officer (KO) or Contracting Officer Representative (COR) to help with this step.</p>"
) %}
<div class="col col--grow">
<form class='search-bar'>
<div class='usa-input search-input'>
<label for='requests-search'>Search requests by Order ID</label>
<input type='search' id='requests-search' name='requests-search' placeholder="Search by Order ID"/>
<button type="submit">
<span class="hide">Search</span>
</button>
</div>
<div class='usa-input'>
<label for='filter-status'>Filter requests by status</label>
<select id="filter-status" name="filter-status">
<option value="" selected disabled>Filter by status</option>
<option value="">Active</option>
<option value="">Pending</option>
<option value="">Denied</option>
</select>
</div>
</form>
<div class='responsive-table-wrapper'>
<table>
<thead>
<tr>
<th scope="col">Order ID</th>
<th scope="col">Request Date</th>
<th scope="col">Requester</th>
<th scope="col">Total Apps</th>
<th scope="col">Status</th>
<th scope="col" class="table-cell--shrink">Actions</th>
</tr>
</thead>
<tbody>
{% for r in requests %}
<tr>
<th scope="row">
<a class='icon-link icon-link--large' href="{{ reverse_url('request_form_update', 1, r['order_id']) if r["status"] != "approved" else reverse_url('financial_verification', r['order_id']) }}">{{ r['order_id'] }}</a>
{% if r['is_new'] %}<span class="usa-label">New</span>
</th>
{% end %}
<td>{{ r['date'] }}</td>
<td>{{ r['full_name'] }}</td>
<td>{{ r['app_count'] }}</td>
<td>{{ r['status'] }}</td>
<td class="table-cell--shrink">
<a href="" class='icon-link'>Download</a>
<a href="/request/approval" class='icon-link'>Approval</a>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
{% end %}

View File

@ -1,4 +1,4 @@
{% extends "../base.html.to" %} {% extends "base.html" %}
{% block content %} {% block content %}
@ -14,15 +14,15 @@
</div> </div>
{% block form_action %} {% block form_action %}
<form method='POST' action="{{ reverse_url('financial_verification', request_id) }}" autocomplete="off"> <form method='POST' action="{{ url_for('requests.financial_verification', request_id=request_id) }}" autocomplete="off">
{% end %} {% endblock %}
{% module xsrf_form_html() %} {{ f.csrf_token }}
{% block form %} {% block form %}
{% autoescape None %} {% autoescape false %}
{% if f.errors %} {% if f.errors %}
<b class="usa-input-error-message">There were some errors, see below.</b> <b class="usa-input-error-message">There were some errors, see below.</b>
{% end %} {% endif %}
<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>
@ -32,7 +32,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.uii_ids.label }} {{ f.uii_ids.label }}
{{ f.uii_ids(placeholder="Example: \nDI 0CVA5786950 \nUN1945326361234786950") }} {{ f.uii_ids(placeholder="Example: \nDI 0CVA5786950 \nUN1945326361234786950") }}
@ -40,7 +40,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.pe_id.label }} {{ f.pe_id.label }}
{{ f.pe_id(placeholder="Example: 0203752A") }} {{ f.pe_id(placeholder="Example: 0203752A") }}
@ -48,7 +48,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.treasury_code.label }} {{ f.treasury_code.label }}
{{ f.treasury_code(placeholder="Example: 1200") }} {{ f.treasury_code(placeholder="Example: 1200") }}
@ -56,7 +56,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.ba_code.label }} {{ f.ba_code.label }}
{{ f.ba_code(placeholder="Example: 02") }} {{ f.ba_code(placeholder="Example: 02") }}
@ -64,7 +64,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
<!-- KO Information --> <!-- KO Information -->
@ -76,7 +76,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.lname_co.label }} {{ f.lname_co.label }}
{{ f.lname_co(placeholder="Contracting Officer last name") }} {{ f.lname_co(placeholder="Contracting Officer last name") }}
@ -84,7 +84,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.email_co.label }} {{ f.email_co.label }}
{{ f.email_co(placeholder="jane@mail.mil") }} {{ f.email_co(placeholder="jane@mail.mil") }}
@ -92,7 +92,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.office_co.label }} {{ f.office_co.label }}
{{ f.office_co(placeholder="Example: WHS") }} {{ f.office_co(placeholder="Example: WHS") }}
@ -100,7 +100,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
<!-- COR Information --> <!-- COR Information -->
@ -113,7 +113,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.lname_cor.label }} {{ f.lname_cor.label }}
{{ f.lname_cor(placeholder="Contracting Officer Representative last name") }} {{ f.lname_cor(placeholder="Contracting Officer Representative last name") }}
@ -121,7 +121,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.email_cor.label }} {{ f.email_cor.label }}
{{ f.email_cor(placeholder="jane@mail.mil") }} {{ f.email_cor(placeholder="jane@mail.mil") }}
@ -129,7 +129,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.office_cor.label }} {{ f.office_cor.label }}
{{ f.office_cor(placeholder="Example: WHS") }} {{ f.office_cor(placeholder="Example: WHS") }}
@ -137,7 +137,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
<br><hr> <br><hr>
<em>&darr; FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available)</em> <em>&darr; FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available)</em>
@ -149,7 +149,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.funding_type_other.label }} {{ f.funding_type_other.label }}
{{ f.funding_type_other(placeholder="") }} {{ f.funding_type_other(placeholder="") }}
@ -157,7 +157,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_0001.label }} {{ f.clin_0001.label }}
{{ f.clin_0001(placeholder="50,000") }} {{ f.clin_0001(placeholder="50,000") }}
@ -165,7 +165,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_0003.label }} {{ f.clin_0003.label }}
{{ f.clin_0003(placeholder="13,000") }} {{ f.clin_0003(placeholder="13,000") }}
@ -173,7 +173,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_1001.label }} {{ f.clin_1001.label }}
{{ f.clin_1001(placeholder="30,000") }} {{ f.clin_1001(placeholder="30,000") }}
@ -181,7 +181,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_1003.label }} {{ f.clin_1003.label }}
{{ f.clin_1003(placeholder="7,000") }} {{ f.clin_1003(placeholder="7,000") }}
@ -189,7 +189,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_2001.label }} {{ f.clin_2001.label }}
{{ f.clin_2001(placeholder="30,000") }} {{ f.clin_2001(placeholder="30,000") }}
@ -197,7 +197,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_2003.label }} {{ f.clin_2003.label }}
{{ f.clin_2003(placeholder="7,000") }} {{ f.clin_2003(placeholder="7,000") }}
@ -205,15 +205,16 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{% end %} {% endautoescape %}
{% endblock form %}
{% block next %} {% block next %}
<input type='submit' class='usa-button usa-button-primary' value='Save & Continue' /> <input type='submit' class='usa-button usa-button-primary' value='Save & Continue' />
{% end %} {% endblock %}
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% end %} {% endblock %}

View File

@ -0,0 +1,13 @@
<div class="progress-menu progress-menu--four">
<ul>
{% for s in screens %}
<li class="progress-menu__item">
<a href="{{ url_for('requests.requests_form_update', screen=loop.index, request_id=request_id) if request_id else url_for('requests.requests_form_new', screen=loop.index) }}"
{% if g.matchesPath('/requests/new/{{ loop.index + 1 }}') %}class="active"{% endif %}
>
{{ s['title'] }}
</a>
</li>
{% endfor %}
</ul>
</div>

View File

@ -1,13 +0,0 @@
<div class="progress-menu progress-menu--four">
<ul>
{% for i,s in enumerate(screens) %}
<li class="progress-menu__item">
<a href="{{ reverse_url('request_form_update', i+1, request_id) if request_id else reverse_url('request_form_new',i+1) }}"
{% if matchesPath('/requests/new/'+str(i+1)) %}class="active"{% end %}
>
{{ s['title'] }}
</a>
</li>
{% end %}
</ul>
</div>

View File

@ -0,0 +1,12 @@
{% extends '../requests_new.html.to' %}
{% block form %}
<h2>New JEDI Request</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus error omnis a, tenetur similique quo officiis voluptates eum recusandae dolorem minus dignissimos, magni consequatur, maxime debitis reprehenderit sint non iusto?</p>
<a class='usa-button usa-button-secondary' href='{{ url_for('requests.request_form_new',next_screen) }}'>New Application</a>
<a class='usa-button usa-button-secondary' href='{{ url_for('requests.request_form_new',next_screen) }}'>Existing Application</a>
<a class='usa-button usa-button-secondary' href='{{ url_for('requests.request_form_new',next_screen) }}'>Sandbox Environment</a>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends '../requests_new.html.to' %}
{% block form %}
<h2>New JEDI Request</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Natus error omnis a, tenetur similique quo officiis voluptates eum recusandae dolorem minus dignissimos, magni consequatur, maxime debitis reprehenderit sint non iusto?</p>
<a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form_new',next_screen) }}'>New Application</a>
<a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form_new',next_screen) }}'>Existing Application</a>
<a class='usa-button usa-button-secondary' href='{{ reverse_url('request_form_new',next_screen) }}'>Sandbox Environment</a>
{% end %}

View File

@ -0,0 +1,46 @@
{% extends 'requests_new.html' %}
{% from "components.html" import Alert, TextInput, OptionsInput %}
{% block subtitle %}
<h2>Details of Use</h2>
{% endblock %}
{% block form %}
{% if f.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
<p>Wed like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.</p>
<p><em>All fields are required, unless specified optional.</em></p>
<h2>General</h2>
{{ TextInput(f.dod_component) }}
{{ TextInput(f.jedi_usage,placeholder="e.g. We are migrating XYZ application to the cloud so that...") }}
<h2>Cloud Readiness</h2>
{{ TextInput(f.num_software_systems,placeholder="Number of systems") }}
{{ OptionsInput(f.jedi_migration) }}
{{ OptionsInput(f.rationalization_software_systems) }}
{{ OptionsInput(f.technical_support_team) }}
{{ OptionsInput(f.organization_providing_assistance) }}
{{ OptionsInput(f.engineering_assessment) }}
{{ TextInput(f.data_transfers) }}
{{ TextInput(f.expected_completion_date) }}
{{ OptionsInput(f.cloud_native) }}
<h2>Financial Usage</h2>
{{ TextInput(f.estimated_monthly_spend) }}
<p>So this means you are spending approximately <b>$X</b> annually</p>
{{ TextInput(f.dollar_value) }}
{{ TextInput(f.number_user_sessions) }}
{{ TextInput(f.average_daily_traffic) }}
{{ TextInput(f.start_date) }}
{% endblock %}

View File

@ -1,45 +0,0 @@
{% extends '../requests_new.html.to' %}
{% block subtitle %}
<h2>Details of Use</h2>
{% end %}
{% block form %}
{% autoescape None %}
{% if f.errors %}
{% module Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) %}
{% end %}
<p>Wed like to know a little about how you plan to use JEDI Cloud services to process your request. Please answer the following questions to the best of your ability. Note that the CCPO does not directly help with migrating systems to JEDI Cloud. These questions are for learning about your cloud readiness and financial usage of the JEDI Cloud; your estimates will not be used for any department level reporting.</p>
<p><em>All fields are required, unless specified optional.</em></p>
<h2>General</h2>
{% module TextInput(f.dod_component) %}
{% module TextInput(f.jedi_usage,placeholder="e.g. We are migrating XYZ application to the cloud so that...") %}
<h2>Cloud Readiness</h2>
{% module TextInput(f.num_software_systems,placeholder="Number of systems") %}
{% module OptionsInput(f.jedi_migration) %}
{% module OptionsInput(f.rationalization_software_systems) %}
{% module OptionsInput(f.technical_support_team) %}
{% module OptionsInput(f.organization_providing_assistance) %}
{% module OptionsInput(f.engineering_assessment) %}
{% module TextInput(f.data_transfers) %}
{% module TextInput(f.expected_completion_date) %}
{% module OptionsInput(f.cloud_native) %}
<h2>Financial Usage</h2>
{% module TextInput(f.estimated_monthly_spend) %}
<p>So this means you are spending approximately <b>$X</b> annually</p>
{% module TextInput(f.dollar_value) %}
{% module TextInput(f.number_user_sessions) %}
{% module TextInput(f.average_daily_traffic) %}
{% module TextInput(f.start_date) %}
{% end %}

View File

@ -0,0 +1,32 @@
{% extends 'requests_new.html' %}
{% from "components.html" import Alert, TextInput, OptionsInput %}
{% block subtitle %}
<h2>Information About You</h2>
{% endblock %}
{% block form %}
{% if f.errors %}
{{ Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) }}
{% endif %}
<p>Please tell us more about you.</p>
{{ TextInput(f.fname_request,placeholder='First Name') }}
{{ TextInput(f.lname_request,placeholder='Last Name') }}
{{ TextInput(f.email_request,placeholder='jane@mail.mil') }}
{{ TextInput(f.phone_number,placeholder='(123) 456-7890') }}
<p>We want to collect the following information from you for security auditing and determining priviledged user access</p>
{{ TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') }}
{{ OptionsInput(f.citizenship) }}
{{ OptionsInput(f.designation) }}
{{ TextInput(f.date_latest_training) }}
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends '../requests_new.html.to' %}
{% block subtitle %}
<h2>Information About You</h2>
{% end %}
{% block form %}
{% autoescape None %}
{% if f.errors %}
{% module Alert('There were some errors',
message="<p>Please see below.</p>",
level='error'
) %}
{% end %}
<p>Please tell us more about you.</p>
{% module TextInput(f.fname_request,placeholder='First Name') %}
{% module TextInput(f.lname_request,placeholder='Last Name') %}
{% module TextInput(f.email_request,placeholder='jane@mail.mil') %}
{% module TextInput(f.phone_number,placeholder='(123) 456-7890') %}
<p>We want to collect the following information from you for security auditing and determining priviledged user access</p>
{% module TextInput(f.service_branch,placeholder='e.g. US Air Force, US Army, US Navy, Marine Corps, Defense Media Agency') %}
{% module OptionsInput(f.citizenship) %}
{% module OptionsInput(f.designation) %}
{% module TextInput(f.date_latest_training) %}
{% end %}

View File

@ -1,18 +1,19 @@
{% extends '../requests_new.html.to' %} {% extends 'requests_new.html' %}
{% from "components.html" import Alert, TextInput %}
{% block subtitle %} {% block subtitle %}
<h2>Primary Government/Military <br> Point of Contact (POC)</h2> <h2>Primary Government/Military <br> Point of Contact (POC)</h2>
{% end %} {% endblock %}
{% block form %} {% block form %}
{% autoescape None %}
{% if f.errors %} {% if f.errors %}
{% module Alert('There were some errors', {{ Alert('There were some errors',
message="<p>Please see below.</p>", message="<p>Please see below.</p>",
level='error' level='error'
) %} ) }}
{% end %} {% endif %}
<p>Please designate a Primary Point of Contact that will be responsible for owning the workspace in the JEDI Cloud.</p> <p>Please designate a Primary Point of Contact that will be responsible for owning the workspace in the JEDI Cloud.</p>
<p>The Point of Contact will become the primary owner of the <em>workspace</em> created to use the JEDI Cloud. As a workspace owner, this person will have the ability to: <p>The Point of Contact will become the primary owner of the <em>workspace</em> created to use the JEDI Cloud. As a workspace owner, this person will have the ability to:
@ -26,9 +27,9 @@
<em>This POC may be you.</em> <em>This POC may be you.</em>
</p> </p>
{% module TextInput(f.fname_poc,placeholder='First Name') %} {{ TextInput(f.fname_poc,placeholder='First Name') }}
{% module TextInput(f.lname_poc,placeholder='Last Name') %} {{ TextInput(f.lname_poc,placeholder='Last Name') }}
{% module TextInput(f.email_poc,placeholder='jane@mail.mil') %} {{ TextInput(f.email_poc,placeholder='jane@mail.mil') }}
{% module TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC') %} {{ TextInput(f.dodid_poc,placeholder='10-digit number on the back of the CAC') }}
{% end %} {% endblock %}

View File

@ -1,27 +1,28 @@
{% extends '../requests_new.html.to' %} {% extends 'requests_new.html' %}
{% from "components.html" import Alert, TextInput %}
{% block subtitle %} {% block subtitle %}
<h2>Review &amp; Submit</h2> <h2>Review &amp; Submit</h2>
{% end %} {% endblock %}
{% block form_action %} {% block form_action %}
<form method='POST' action="{{ reverse_url('requests_submit', request_id) }}" autocomplete="off"> <form method='POST' action="{{ url_for('requests.requests_submit', request_id=request_id) }}" autocomplete="off">
{% end %} {% endblock %}
{% block form %} {% block form %}
{% autoescape None %}
{% if f.errors %} {% if f.errors %}
{% module Alert('There were some errors', {{ Alert('There were some errors',
message="<p>Please complete all required fields before submitting.</p>", message="<p>Please complete all required fields before submitting.</p>",
level='error' level='error'
) %} ) }}
{% end %} {% endif %}
<p>Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.</p> <p>Before you can submit your request, please take a moment to review the information entered in the form. You may make changes by clicking the edit link on each section. When all information looks right, go ahead and submit.</p>
<h2>Details of Use <a href="{{ reverse_url('request_form_update', 1, request_id) }}" class="icon-link">Edit</a></h2> <h2>Details of Use <a href="{{ url_for('requests.requests_form_update', screen=1, request_id=request_id) }}" class="icon-link">Edit</a></h2>
<dl> <dl>
<div> <div>
@ -107,7 +108,7 @@
</dl> </dl>
<h2>Information About You <a href="{{ reverse_url('request_form_update', 2, request_id) }}" class="icon-link">Edit</a></h2> <h2>Information About You <a href="{{ url_for('requests.requests_form_update', screen=2, request_id=request_id) }}" class="icon-link">Edit</a></h2>
<dl> <dl>
<div> <div>
@ -152,7 +153,7 @@
</dl> </dl>
<h2>Primary Point of Contact <a href="{{ reverse_url('request_form_update', 3, request_id) }}" class="icon-link">Edit</a></h2> <h2>Primary Point of Contact <a href="{{ url_for('requests.requests_form_update', screen=3, request_id=request_id) }}" class="icon-link">Edit</a></h2>
<dl> <dl>
<div> <div>
@ -178,15 +179,15 @@
{% end %} {% endblock %}
{% block next %} {% block next %}
{% if not can_submit %} {% if not can_submit %}
{% module Alert('There were some errors', {{ Alert('There were some errors',
message="<p>Please complete all required fields before submitting.</p>", message="<p>Please complete all required fields before submitting.</p>",
level='error' level='error'
) %} ) }}
{% end %} {% endif %}
<div class='action-group'> <div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Submit' {{ "disabled" if not can_submit else "" }} /> <input type='submit' class='usa-button usa-button-primary' value='Submit' {{ "disabled" if not can_submit else "" }} />
@ -194,4 +195,4 @@
</form> </form>
{% end %} {% endblock %}

View File

@ -2,13 +2,12 @@
{% block form %} {% block form %}
{% autoescape None %}
{% if f.errors %} {% if f.errors %}
{% module Alert('There were some errors', {{ Alert('There were some errors',
message="<p>Please complete all the fields before submitting.</p>", message="<p>Please complete all the fields before submitting.</p>",
level='error' level='error'
) %} ) }}
{% end %} {% endif %}
<h2 id="financial-verification">Financial Verification</h2> <h2 id="financial-verification">Financial Verification</h2>
@ -20,7 +19,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.uii_ids.label }} {{ f.uii_ids.label }}
{{ f.uii_ids(placeholder="Example: \nDI 0CVA5786950 \nUN1945326361234786950") }} {{ f.uii_ids(placeholder="Example: \nDI 0CVA5786950 \nUN1945326361234786950") }}
@ -28,7 +27,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.pe_id.label }} {{ f.pe_id.label }}
{{ f.pe_id(placeholder="Example: 0203752A") }} {{ f.pe_id(placeholder="Example: 0203752A") }}
@ -36,7 +35,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.treasury_code.label }} {{ f.treasury_code.label }}
{{ f.treasury_code(placeholder="Example: 1200") }} {{ f.treasury_code(placeholder="Example: 1200") }}
@ -44,7 +43,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.ba_code.label }} {{ f.ba_code.label }}
{{ f.ba_code(placeholder="Example: 02") }} {{ f.ba_code(placeholder="Example: 02") }}
@ -52,7 +51,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
<!-- KO Information --> <!-- KO Information -->
@ -64,7 +63,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.lname_co.label }} {{ f.lname_co.label }}
{{ f.lname_co(placeholder="Contracting Officer last name") }} {{ f.lname_co(placeholder="Contracting Officer last name") }}
@ -72,7 +71,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.email_co.label }} {{ f.email_co.label }}
{{ f.email_co(placeholder="jane@mail.mil") }} {{ f.email_co(placeholder="jane@mail.mil") }}
@ -80,7 +79,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.office_co.label }} {{ f.office_co.label }}
{{ f.office_co(placeholder="Example: WHS") }} {{ f.office_co(placeholder="Example: WHS") }}
@ -88,7 +87,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
<!-- COR Information --> <!-- COR Information -->
@ -101,7 +100,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.lname_cor.label }} {{ f.lname_cor.label }}
{{ f.lname_cor(placeholder="Contracting Officer Representative last name") }} {{ f.lname_cor(placeholder="Contracting Officer Representative last name") }}
@ -109,7 +108,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.email_cor.label }} {{ f.email_cor.label }}
{{ f.email_cor(placeholder="jane@mail.mil") }} {{ f.email_cor(placeholder="jane@mail.mil") }}
@ -117,7 +116,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.office_cor.label }} {{ f.office_cor.label }}
{{ f.office_cor(placeholder="Example: WHS") }} {{ f.office_cor(placeholder="Example: WHS") }}
@ -125,7 +124,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
<br><hr> <br><hr>
<em>&darr; FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available)</em> <em>&darr; FIELDS NEEDED FOR MANUAL ENTRY OF TASK ORDER INFORMATION (only necessary if EDA info not available)</em>
@ -137,7 +136,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.funding_type_other.label }} {{ f.funding_type_other.label }}
{{ f.funding_type_other(placeholder="") }} {{ f.funding_type_other(placeholder="") }}
@ -145,7 +144,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_0001.label }} {{ f.clin_0001.label }}
{{ f.clin_0001(placeholder="50,000") }} {{ f.clin_0001(placeholder="50,000") }}
@ -153,7 +152,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_0003.label }} {{ f.clin_0003.label }}
{{ f.clin_0003(placeholder="13,000") }} {{ f.clin_0003(placeholder="13,000") }}
@ -161,7 +160,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_1001.label }} {{ f.clin_1001.label }}
{{ f.clin_1001(placeholder="30,000") }} {{ f.clin_1001(placeholder="30,000") }}
@ -169,7 +168,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_1003.label }} {{ f.clin_1003.label }}
{{ f.clin_1003(placeholder="7,000") }} {{ f.clin_1003(placeholder="7,000") }}
@ -177,7 +176,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_2001.label }} {{ f.clin_2001.label }}
{{ f.clin_2001(placeholder="30,000") }} {{ f.clin_2001(placeholder="30,000") }}
@ -185,7 +184,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{{ f.clin_2003.label }} {{ f.clin_2003.label }}
{{ f.clin_2003(placeholder="7,000") }} {{ f.clin_2003(placeholder="7,000") }}
@ -193,7 +192,7 @@
<div class="usa-input-error-message"> <div class="usa-input-error-message">
{{ e }} {{ e }}
</div> </div>
{% end %} {% endfor %}
{% end %} {% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block content %}
<div class="col">
{% include 'requests/menu.html' %}
<div class="panel">
<div class="panel__heading">
<h1>New Request</h1>
<div class="subtitle">{% block subtitle %}{% endblock %}</div>
</div>
<div class="panel__content">
{% block form_action %}
{% if request_id %}
<form method='POST' action="{{ url_for('requests.requests_form_update', screen=current, request_id=request_id) }}" autocomplete="off">
{% else %}
<form method='POST' action="{{ url_for('requests.requests_form_update', screen=current) }}" autocomplete="off">
{% endif %}
{% endblock %}
{{ f.csrf_token }}
{% block form %}
form goes here
{% endblock %}
</div>
</div>
{% block next %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Save & Continue' />
</div>
{% endblock %}
</form>
</div>
{% endblock %}

View File

@ -1,46 +0,0 @@
{% extends "base.html.to" %}
{% block content %}
<div class="col">
{% include 'requests/menu.html.to' %}
<div class="panel">
<div class="panel__heading">
<h1>New Request</h1>
<div class="subtitle">{% block subtitle %}{% end %}</div>
</div>
<div class="panel__content">
{% block form_action %}
{% if request_id %}
<form method='POST' action="{{ reverse_url('request_form_update', current, request_id) }}" autocomplete="off">
{% else %}
<form method='POST' action="{{ reverse_url('request_form_new', current) }}" autocomplete="off">
{% end %}
{% end %}
{% module xsrf_form_html() %}
{% block form %}
form goes here
{% end %}
</div>
</div>
{% block next %}
<div class='action-group'>
<input type='submit' class='usa-button usa-button-primary' value='Save & Continue' />
</div>
{% end %}
</form>
</div>
{% end %}

View File

@ -3,10 +3,10 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{% block title %}JEDI{% end %}</title> <title>{% block title %}JEDI{% endblock %}</title>
{% for url in assets['css'].urls() %} {% assets "css" %}
<link rel="stylesheet" href="{{ url }}" type="text/css"> <link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% end %} {% endassets %}
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
</head> </head>
<body> <body>
@ -17,11 +17,11 @@
<h1 class="usa-display">JEDI</h1> <h1 class="usa-display">JEDI</h1>
<a class="usa-button" href='{{ config['default'].get('cac_url','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a> <a class="usa-button" href='{{ config.get('cac_url','https://cac.atat.codes') }}'><span>Sign In with CAC</span></a>
<button class="usa-button" disabled>Sign In via MFA</button> <button class="usa-button" disabled>Sign In via MFA</button>
{% if dev() %} {% if g.dev %}
<a class="usa-button usa-button-secondary" href='/login-dev'><span>DEV Login</span></a> <a class="usa-button usa-button-secondary" href='/login-dev'><span>DEV Login</span></a>
{% end %} {% endif %}
</main> </main>

View File

@ -1,8 +1,10 @@
{% extends "base.html.to" %} {% extends "base.html" %}
{% from "components.html" import Alert, Modal, Icon %}
{% block modal %} {% block modal %}
{% if modalOpen() %} {% if g.modalOpen %}
{% apply modal %} {% call Modal() %}
<h1>A modal dialog</h1> <h1>A modal dialog</h1>
<p>We count thirty Rebel ships, Lord Vader. But they're so small they're evading our turbo-lasers! We'll have to destroy them ship to ship. Get the crews to their fighters. Luke, let me know when you're going in. I'm on my way in now... Watch yourself! There's a lot of fire coming from the right side of that deflection tower. I'm on it. Squad leaders, we've picked up a new group of signals. Enemy fighters coming your way.</p> <p>We count thirty Rebel ships, Lord Vader. But they're so small they're evading our turbo-lasers! We'll have to destroy them ship to ship. Get the crews to their fighters. Luke, let me know when you're going in. I'm on my way in now... Watch yourself! There's a lot of fire coming from the right side of that deflection tower. I'm on it. Squad leaders, we've picked up a new group of signals. Enemy fighters coming your way.</p>
@ -12,35 +14,35 @@
<a href='/styleguide' class='action-group__action usa-button'>Close</a> <a href='/styleguide' class='action-group__action usa-button'>Close</a>
<a href='/styleguide' class='action-group__action'>This also closes the modal</a> <a href='/styleguide' class='action-group__action'>This also closes the modal</a>
</div> </div>
{% end %} {% endcall %}
{% end %} {% endif %}
{% end %} {% endblock %}
{% block content %} {% block content %}
{% module Alert('A Warning Alert', {{ Alert('A Warning Alert',
message="\ message="\
<p>This is a message. It is a very important message. Please note, <strong>proper semantic markup is required</strong> here, such as paragraph tags. Don't omit paragraph tags!</p>\ <p>This is a message. It is a very important message. Please note, <strong>proper semantic markup is required</strong> here, such as paragraph tags. Don't omit paragraph tags!</p>\
<p>Also note the same for actions below. You'll need to include the full link markup.</p>\ <p>Also note the same for actions below. You'll need to include the full link markup.</p>\
", ",
actions="<a href='/styleguide?modal=True'>Open a Modal Dialog</a>", actions="<a href='/styleguide?modal=True'>Open a Modal Dialog</a>",
level='warning' level='warning'
) %} ) }}
{% module Alert('A Success Alert', {{ Alert('A Success Alert',
message="<p>Congratulations! You did a thing.</p>", message="<p>Congratulations! You did a thing.</p>",
level='success' level='success'
) %} ) }}
{% module Alert('An Error Alert', {{ Alert('An Error Alert',
message="<p>Booooo. You're the worst.</p>", message="<p>Booooo. You're the worst.</p>",
level='error' level='error'
) %} ) }}
{% module Alert('An Info Alert', {{ Alert('An Info Alert',
message="<p>The more you know.</p>" message="<p>The more you know.</p>"
) %} ) }}
<nav class='sidenav'> <nav class='sidenav'>
<ul> <ul>
@ -146,7 +148,7 @@
<span class='usa-input__help'> <span class='usa-input__help'>
This is some help text to explain what this form field is and why you should fill it out. This is some help text to explain what this form field is and why you should fill it out.
</span> </span>
{% module Icon('alert') %} {{ Icon('alert') }}
</label> </label>
<input id='basic-text-2' type='text' placeholder='this is a sample of what you should enter' aria-invalid='true' aria-describedby='basic-text-2__errors'/> <input id='basic-text-2' type='text' placeholder='this is a sample of what you should enter' aria-invalid='true' aria-describedby='basic-text-2__errors'/>
<span id='basic-text-2__errors' class='usa-input__message'>Oh boy you really screwed up big time, didn't you!</span> <span id='basic-text-2__errors' class='usa-input__message'>Oh boy you really screwed up big time, didn't you!</span>
@ -158,7 +160,7 @@
<span class='usa-input__help'> <span class='usa-input__help'>
This is some help text to explain what this form field is and why you should fill it out. This is some help text to explain what this form field is and why you should fill it out.
</span> </span>
{% module Icon('alert') %} {{ Icon('alert') }}
</label> </label>
<input id='basic-text-3' type='text' placeholder='this is a sample of what you should enter' aria-describedby='basic-text-3__errors'/> <input id='basic-text-3' type='text' placeholder='this is a sample of what you should enter' aria-describedby='basic-text-3__errors'/>
<span id='basic-text-3__errors' class='usa-input__message'>Oh boy you really screwed up big time, didn't you!</span> <span id='basic-text-3__errors' class='usa-input__message'>Oh boy you really screwed up big time, didn't you!</span>
@ -170,7 +172,7 @@
<span class='usa-input__help'> <span class='usa-input__help'>
This is some help text to explain what this form field is and why you should fill it out. This is some help text to explain what this form field is and why you should fill it out.
</span> </span>
{% module Icon('ok') %} {{ Icon('ok') }}
</label> </label>
<input id='basic-text-4' type='text' placeholder='this is a sample of what you should enter'/> <input id='basic-text-4' type='text' placeholder='this is a sample of what you should enter'/>
</div> </div>
@ -214,7 +216,7 @@
<div class='form-col'> <div class='form-col'>
<div class='usa-input usa-input--error'> <div class='usa-input usa-input--error'>
<fieldset class='usa-input__choices usa-input__choices--inline'> <fieldset class='usa-input__choices usa-input__choices--inline'>
<legend>Inline Checkboxes {% module Icon('alert') %}</legend> <legend>Inline Checkboxes {{ Icon('alert') }}</legend>
<input type='checkbox' id='checkbox-4'/> <input type='checkbox' id='checkbox-4'/>
<label for='checkbox-4'>Checkbox Four</label> <label for='checkbox-4'>Checkbox Four</label>
@ -234,7 +236,7 @@
<div class='form-col'> <div class='form-col'>
<div class='usa-input usa-input--error'> <div class='usa-input usa-input--error'>
<fieldset class='usa-input__choices' > <fieldset class='usa-input__choices' >
<legend>Problem Radio Buttons {% module Icon('alert') %}</legend> <legend>Problem Radio Buttons {{ Icon('alert') }}</legend>
<input type='radio' name='radio' id='radio-1' /> <input type='radio' name='radio' id='radio-1' />
<label for='radio-1'>Radio One</label> <label for='radio-1'>Radio One</label>
@ -279,28 +281,30 @@
<div class='panel'> <div class='panel'>
<div class='panel__content'> <div class='panel__content'>
<h5>Icons</h5> <h5>Icons</h5>
<span class='nowrap'>{% module Icon('trash') %} 'trash'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('trash') }} 'trash'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('document') %} 'document'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('document') }} 'document'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('cloud') %} 'cloud'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('cloud') }} 'cloud'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('chart') %} 'chart'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('chart') }} 'chart'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('caret_up') %} 'caret_up'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('caret_up') }} 'caret_up'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('caret_down') %} 'caret_down'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('caret_down') }} 'caret_down'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('caret_right') %} 'caret_right'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('caret_right') }} 'caret_right'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('caret_left') %} 'caret_left'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('caret_left') }} 'caret_left'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('x') %} 'x'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('x') }} 'x'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('search') %} 'search'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('search') }} 'search'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('avatar') %} 'avatar'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('avatar') }} 'avatar'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('download') %} 'download'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('download') }} 'download'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('briefcase') %} 'briefcase'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('briefcase') }} 'briefcase'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('bell') %} 'bell'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('bell') }} 'bell'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('folder') %} 'folder'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('folder') }} 'folder'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('help') %} 'help'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('help') }} 'help'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('shield') %} 'shield'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('shield') }} 'shield'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('info') %} 'info'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('info') }} 'info'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('alert') %} 'alert'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('alert') }} 'alert'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('link') %} 'link'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('link') }} 'link'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('ok') %} 'ok'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('ok') }} 'ok'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{% module Icon('checkmark') %} 'checkmark'&nbsp;&nbsp;&nbsp;</span> <span class='nowrap'>{{ Icon('checkmark') }} 'checkmark'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{{ Icon('arrow-right') }} 'arrow-right'&nbsp;&nbsp;&nbsp;</span>
<span class='nowrap'>{{ Icon('arrow-down') }} 'arrow-down'&nbsp;&nbsp;&nbsp;</span>
</div> </div>
</div> </div>
@ -358,4 +362,4 @@
<a href='/styleguide' class='action-group__action'>Action group link</a> <a href='/styleguide' class='action-group__action'>Action group link</a>
</div> </div>
</div> </div>
{% end %} {% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "base.html.to" %} {% extends "base.html" %}
{% block content %} {% block content %}
@ -8,5 +8,5 @@
</main> </main>
{% end %} {% endblock %}

View File

@ -0,0 +1,75 @@
{% extends "base_workspace.html.to" %}
{% block workspace_content %}
{% if not members %}
{% module EmptyState(
'There are currently no members in this Workspace.',
actionLabel='Invite a new Member',
actionHref='/members/new',
icon='avatar'
)%}
{% else %}
<form class='search-bar'>
<div class='usa-input search-input'>
<label for='members-search'>Search members by name</label>
<input type='search' id='members-search' name='members-search' placeholder="Search by name"/>
<button type="submit">
<span class="hide">Search</span>
</button>
</div>
<div class='usa-input filter-input'>
<label for='filter-status'>Filter members by status</label>
<select id="filter-status" name="filter-status">
<option value="" selected disabled>Filter by status</option>
<option value="">Active</option>
<option value="">Pending</option>
<option value="">Denied</option>
</select>
</div>
<div class='usa-input filter-input'>
<label for='filter-role'>Filter members by role</label>
<select id="filter-role" name="filter-role">
<option value="" selected disabled>Filter by role</option>
<option value="">Admin</option>
<option value="">CCPO</option>
<option value="">Developer</option>
</select>
</div>
</form>
<div class='responsive-table-wrapper'>
<table>
<thead>
<tr>
<th scope="col" width="50%">Name</th>
<th scope="col" class='table-cell--shrink'>&nbsp;<span class="hide">Status Flag</span></th>
<th scope="col">Status</th>
<th scope="col">Workspace Role</th>
</tr>
</thead>
<tbody>
{% for m in members %}
<tr>
<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> {% end %}</td>
<td>{{ m['status'] }}</a></td>
<td>{{ m['workspace_role'] }}</a></td>
</tr>
{% end %}
</tbody>
</table>
</div>
{% end %}
{% end %}

View File

@ -1,93 +0,0 @@
{% extends "base_workspace.html.to" %}
{% block workspace_content %}
<form class='search-bar'>
<div class='usa-input search-input'>
<label for='members-search'>Search members by name</label>
<input type='search' id='members-search' name='members-search' placeholder="Search by name"/>
<button type="submit">
<span class="hide">Search</span>
</button>
</div>
<div class='usa-input filter-input'>
<label for='filter-status'>Filter members by status</label>
<select id="filter-status" name="filter-status">
<option value="" selected disabled>Filter by status</option>
<option value="">Active</option>
<option value="">Pending</option>
<option value="">Denied</option>
</select>
</div>
<div class='usa-input filter-input'>
<label for='filter-role'>Filter members by role</label>
<select id="filter-role" name="filter-role">
<option value="" selected disabled>Filter by role</option>
<option value="">Admin</option>
<option value="">CCPO</option>
<option value="">Developer</option>
</select>
</div>
</form>
{# <div class='panel panel__actions'>
<div class='row'>
<div class='col col--grow col--pad'>
<form class="usa-search usa-search-small">
<label class="usa-sr-only" for="search-field-small">Search small</label>
<input id="search-field-small" type="search" name="search" placeholder="Search by name">
<button type="submit">
<span class="usa-sr-only">Search</span>
</button>
</form>
</div>
<div class='col col--grow col--pad'>
<select id="filter_status" name="filter_status" required="">
<option value="" selected disabled>Filter by status</option>
<option value="">Active</option>
<option value="">Pending</option>
<option value="">Denied</option>
</select>
</div>
<div class='col col--grow col--pad'>
<select id="filter_role" name="filter_role" required="">
<option value="" selected disabled>Filter by role</option>
<option value="">Admin</option>
<option value="">CCPO</option>
<option value="">Developer</option>
</select>
</div>
</div>
</div> #}
<div class='responsive-table-wrapper'>
<table>
<thead>
<tr>
<th scope="col" width="50%">Name</th>
<th scope="col" class='table-cell--shrink'>&nbsp;<span class="hide">Status Flag</span></th>
<th scope="col">Status</th>
<th scope="col">Workspace Role</th>
</tr>
</thead>
<tbody>
{% for m in members %}
<tr>
<td><a href="#" 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> {% end %}</td>
<td>{{ m['status'] }}</a></td>
<td>{{ m['workspace_role'] }}</a></td>
</tr>
{% end %}
</tbody>
</table>
</div>
{% end %}

View File

@ -1,13 +1,15 @@
{% extends "base_workspace.html.to" %} {% from "components.html" import Icon %}
{% extends "base_workspace.html" %}
{% block workspace_content %} {% block workspace_content %}
{% for project in projects %} {% for project in 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'] }} ({{ len(project['environments'])}} environments)</h2> <h2 class='block-list__title'>{{ project['name'] }} ({{ project['environments']|length }} environments)</h2>
<a class='block-list__header__link' href='/workspaces/123456/projects/789/edit'> <a class='icon-link' href='/workspaces/123456/projects/789/edit'>
{% module Icon('edit') %} {{ Icon('edit') }}
<span>edit</span> <span>edit</span>
</a> </a>
</header> </header>
@ -15,7 +17,7 @@
{% 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'>
{% module Icon('link') %} {{ Icon('link') }}
<span>{{ environment["name"]}}</span> <span>{{ environment["name"]}}</span>
</a> </a>
@ -24,10 +26,10 @@
<span>members</span> <span>members</span>
</div> </div>
</li> </li>
{% end %} {% endfor %}
</ul> </ul>
</div> </div>
{% end %} {% endfor %}
{% end %} {% endblock %}

31
templates/workspaces.html Normal file
View File

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block content %}
<div class='col'>
<table>
<thead>
<tr>
<th>Workspace Name</th>
<th>Task Order</th>
<th>Users</th>
</tr>
</thead>
<tbody>
{% for w in workspaces %}
<tr>
<td>
<a class='icon-link icon-link--large' href="/workspaces/{{w['task_order']['number']}}/projects">{{ w['name'] }}</a><br>
</td>
<td>
#{{ w['task_order']['number'] }}
</td>
<td>
<span class="label">{{ w['user_count'] }}</span><span class='h6'>Users</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,30 +0,0 @@
{% extends "base.html.to" %}
{% block content %}
<div class='col'>
<table>
<thead>
<tr>
<th scope="col" width="50%">Workspace Name</th>
<th scope="col" width="30%">Workspace Info</th>
<th scope="col" width="20%">Actions</th>
</tr>
</thead>
<tbody>
{% for w in workspaces %}
<tr>
<td scope="row">
<a href="/workspaces/{{w['task_order']['number']}}/projects">{{ w['name'] }}</a><br>
Task Order: #{{ w['task_order']['number'] }}
</td>
<td>
<span class="usa-label">{{ w['user_count'] }}</span><br>Users
</td>
<td><button class="usa-button-secondary">Launch</button></td>
</tr>
{% end %}
</tbody>
</table>
</div>
{% end %}

View File

@ -1,44 +1,72 @@
import os
import pytest import pytest
import alembic.config
import alembic.command
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session from sqlalchemy.orm import sessionmaker, scoped_session
from atst.app import make_app, make_deps, make_config from atst.app import make_app, make_config
from atst.database import make_db
from tests.mocks import MockApiClient from tests.mocks import MockApiClient
from atst.sessions import DictSessions from atst.sessions import DictSessions
from atst.models import Base
from atst.database import db as _db
@pytest.fixture @pytest.fixture(scope='session')
def app(db): def app(request):
TEST_DEPS = {
"authnid_client": MockApiClient("authnid"),
"sessions": DictSessions(),
"db_session": db
}
config = make_config() config = make_config()
deps = make_deps(config)
deps.update(TEST_DEPS)
return make_app(config, deps) _app = make_app(config)
ctx = _app.app_context()
ctx.push()
def teardown():
ctx.pop()
return _app
@pytest.fixture(scope='function') def apply_migrations():
def db(): """Applies all alembic migrations."""
alembic_config = os.path.join(os.path.dirname(__file__), "../", "alembic.ini")
config = alembic.config.Config(alembic_config)
app_config = make_config()
config.set_main_option('sqlalchemy.url', app_config["DATABASE_URI"])
alembic.command.upgrade(config, 'head')
# Override db with a new SQLAlchemy session so that we can rollback
# each test's transaction. @pytest.fixture(scope='session')
# Inspiration: https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#session-external-transaction def db(app, request):
config = make_config()
database = make_db(config) def teardown():
connection = database.get_bind().connect() _db.drop_all()
_db.app = app
apply_migrations()
yield _db
_db.drop_all()
@pytest.fixture(scope='function', autouse=True)
def session(db, request):
"""Creates a new database session for a test."""
connection = db.engine.connect()
transaction = connection.begin() transaction = connection.begin()
db = scoped_session(sessionmaker(bind=connection))
yield db options = dict(bind=connection, binds={})
session = db.create_scoped_session(options=options)
db.session = session
yield session
db.close()
transaction.rollback() transaction.rollback()
connection.close() connection.close()
session.remove()
class DummyForm(dict): class DummyForm(dict):

View File

@ -6,36 +6,32 @@ from atst.domain.pe_numbers import PENumbers
from tests.factories import PENumberFactory from tests.factories import PENumberFactory
@pytest.fixture()
def pe_numbers(db):
return PENumbers(db)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def new_pe_number(db): def new_pe_number(session):
def make_pe_number(**kwargs): def make_pe_number(**kwargs):
pen = PENumberFactory.create(**kwargs) pen = PENumberFactory.create(**kwargs)
db.add(pen) session.add(pen)
db.commit() session.commit()
return pen return pen
return make_pe_number return make_pe_number
def test_can_get_pe_number(pe_numbers, new_pe_number): def test_can_get_pe_number(new_pe_number):
new_pen = new_pe_number(number="0701367F", description="Combat Support - Offensive") new_pen = new_pe_number(number="0701367F", description="Combat Support - Offensive")
pen = pe_numbers.get(new_pen.number) pen = PENumbers.get(new_pen.number)
assert pen.number == new_pen.number assert pen.number == new_pen.number
def test_nonexistent_pe_number_raises(pe_numbers): def test_nonexistent_pe_number_raises():
with pytest.raises(NotFoundError): with pytest.raises(NotFoundError):
pe_numbers.get("some fake number") PENumbers.get("some fake number")
def test_create_many(pe_numbers): def test_create_many():
pen_list = [['123456', 'Land Speeder'], ['7891011', 'Lightsaber']] pen_list = [['123456', 'Land Speeder'], ['7891011', 'Lightsaber']]
pe_numbers.create_many(pen_list) PENumbers.create_many(pen_list)
assert pe_numbers.get(pen_list[0][0]) assert PENumbers.get(pen_list[0][0])
assert pe_numbers.get(pen_list[1][0]) assert PENumbers.get(pen_list[1][0])

View File

@ -8,14 +8,14 @@ from tests.factories import RequestFactory
@pytest.fixture() @pytest.fixture()
def requests(db): def requests(session):
return Requests(db) return Requests()
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def new_request(db): def new_request(session):
created_request = RequestFactory.create() created_request = RequestFactory.create()
db.add(created_request) session.add(created_request)
db.commit() session.commit()
return created_request return created_request
@ -31,25 +31,22 @@ def test_nonexistent_request_raises(requests):
requests.get(uuid4()) requests.get(uuid4())
@pytest.mark.gen_test
def test_auto_approve_less_than_1m(requests, new_request): def test_auto_approve_less_than_1m(requests, new_request):
new_request.body = {"details_of_use": {"dollar_value": 999999}} new_request.body = {"details_of_use": {"dollar_value": 999999}}
request = yield requests.submit(new_request) request = requests.submit(new_request)
assert request.status == 'approved' assert request.status == 'approved'
@pytest.mark.gen_test
def test_dont_auto_approve_if_dollar_value_is_1m_or_above(requests, new_request): def test_dont_auto_approve_if_dollar_value_is_1m_or_above(requests, new_request):
new_request.body = {"details_of_use": {"dollar_value": 1000000}} new_request.body = {"details_of_use": {"dollar_value": 1000000}}
request = yield requests.submit(new_request) request = requests.submit(new_request)
assert request.status == 'submitted' assert request.status == 'submitted'
@pytest.mark.gen_test
def test_dont_auto_approve_if_no_dollar_value_specified(requests, new_request): def test_dont_auto_approve_if_no_dollar_value_specified(requests, new_request):
new_request.body = {"details_of_use": {}} new_request.body = {"details_of_use": {}}
request = yield requests.submit(new_request) request = requests.submit(new_request)
assert request.status == 'submitted' assert request.status == 'submitted'

View File

@ -4,8 +4,8 @@ from atst.domain.exceptions import NotFoundError
@pytest.fixture() @pytest.fixture()
def roles_repo(db): def roles_repo(session):
return Roles(db) return Roles(session)
def test_get_all_roles(roles_repo): def test_get_all_roles(roles_repo):

View File

@ -7,15 +7,15 @@ from tests.factories import TaskOrderFactory
@pytest.fixture() @pytest.fixture()
def task_orders(db): def task_orders(session):
return TaskOrders(db) return TaskOrders(session)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def new_task_order(db): def new_task_order(session):
def make_task_order(**kwargs): def make_task_order(**kwargs):
to = TaskOrderFactory.create(**kwargs) to = TaskOrderFactory.create(**kwargs)
db.add(to) session.add(to)
db.commit() session.commit()
return to return to

View File

@ -6,8 +6,8 @@ from atst.domain.exceptions import NotFoundError, AlreadyExistsError
@pytest.fixture() @pytest.fixture()
def users_repo(db): def users_repo(session):
return Users(db) return Users(session)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")

View File

@ -6,13 +6,13 @@ from atst.domain.users import Users
@pytest.fixture() @pytest.fixture()
def users_repo(db): def users_repo(session):
return Users(db) return Users(session)
@pytest.fixture() @pytest.fixture()
def workspace_users_repo(db): def workspace_users_repo(session):
return WorkspaceUsers(db) return WorkspaceUsers(session)
def test_can_create_new_workspace_user(users_repo, workspace_users_repo): def test_can_create_new_workspace_user(users_repo, workspace_users_repo):

View File

@ -0,0 +1,25 @@
import pytest
from wtforms import Form
import pendulum
from atst.forms.fields import DateField
class MyForm(Form):
date = DateField()
def test_date_ie_format():
form = MyForm(data={"date": "12/24/2018"})
assert form.date._value() == pendulum.date(2018, 12, 24)
def test_date_sane_format():
form = MyForm(data={"date": "2018-12-24"})
assert form.date._value() == pendulum.date(2018, 12, 24)
def test_date_insane_format():
form = MyForm(data={"date": "hello"})
with pytest.raises(ValueError):
form.date._value()

View File

@ -1,89 +0,0 @@
import re
import pytest
import tornado
import urllib
from tests.mocks import MOCK_REQUEST, MOCK_USER
from tests.factories import PENumberFactory
class TestPENumberInForm:
required_data = {
"pe_id": "123",
"task_order_id": "1234567899C0001",
"fname_co": "Contracting",
"lname_co": "Officer",
"email_co": "jane@mail.mil",
"office_co": "WHS",
"fname_cor": "Officer",
"lname_cor": "Representative",
"email_cor": "jane@mail.mil",
"office_cor": "WHS",
"funding_type": "RDTE",
"funding_type_other": "other",
"clin_0001": "50,000",
"clin_0003": "13,000",
"clin_1001": "30,000",
"clin_1003": "7,000",
"clin_2001": "30,000",
"clin_2003": "7,000",
}
def _set_monkeypatches(self, monkeypatch):
monkeypatch.setattr(
"atst.handlers.request_financial_verification.RequestFinancialVerification.get_current_user", lambda s: MOCK_USER
)
monkeypatch.setattr(
"atst.handlers.request_financial_verification.RequestFinancialVerification.check_xsrf_cookie", lambda s: True
)
monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True)
monkeypatch.setattr("atst.domain.requests.Requests.get", lambda s, i: MOCK_REQUEST)
@tornado.gen.coroutine
def submit_data(self, http_client, base_url, data):
response = yield http_client.fetch(
base_url + "/requests/verify/{}".format(MOCK_REQUEST.id),
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body=urllib.parse.urlencode(data),
follow_redirects=False,
raise_error=False,
)
return response
@pytest.mark.gen_test
def test_submit_request_form_with_invalid_pe_id(self, monkeypatch, http_client, base_url):
self._set_monkeypatches(monkeypatch)
response = yield self.submit_data(http_client, base_url, self.required_data)
assert "We couldn\'t find that PE number" in response.body.decode()
assert response.code == 200
assert "/requests/verify" in response.effective_url
@pytest.mark.gen_test
def test_submit_request_form_with_unchanged_pe_id(self, monkeypatch, http_client, base_url):
self._set_monkeypatches(monkeypatch)
data = dict(self.required_data)
data['pe_id'] = MOCK_REQUEST.body['financial_verification']['pe_id']
response = yield self.submit_data(http_client, base_url, data)
assert response.code == 302
assert response.headers.get("Location") == "/requests/financial_verification_submitted"
@pytest.mark.gen_test
def test_submit_request_form_with_new_valid_pe_id(self, db, monkeypatch, http_client, base_url):
self._set_monkeypatches(monkeypatch)
pe = PENumberFactory.create(number="8675309U", description="sample PE number")
db.add(pe)
db.commit()
data = dict(self.required_data)
data['pe_id'] = pe.number
response = yield self.submit_data(http_client, base_url, data)
assert response.code == 302
assert response.headers.get("Location") == "/requests/financial_verification_submitted"

View File

@ -1,54 +0,0 @@
import re
import pytest
import tornado
import urllib
from tests.mocks import MOCK_USER
from tests.factories import RequestFactory
ERROR_CLASS = "alert--error"
MOCK_REQUEST = RequestFactory.create(
creator=MOCK_USER["id"],
body={
"financial_verification": {
"pe_id": "0203752A",
},
}
)
@pytest.mark.gen_test
def test_submit_invalid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER
)
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True
)
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/new",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="total_ram=5",
)
assert response.effective_url == base_url + "/requests/new"
assert re.search(ERROR_CLASS, response.body.decode())
@pytest.mark.gen_test
def test_submit_valid_request_form(monkeypatch, http_client, base_url):
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.get_current_user", lambda s: MOCK_USER
)
monkeypatch.setattr(
"atst.handlers.request_new.RequestNew.check_xsrf_cookie", lambda s: True
)
monkeypatch.setattr("atst.forms.request.RequestForm.validate", lambda s: True)
# this just needs to send a known invalid form value
response = yield http_client.fetch(
base_url + "/requests/new",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
body="meaning=42",
)
assert "/requests/new/2" in response.effective_url

Some files were not shown because too many files have changed in this diff Show More