Merge branch 'dev' into feature/parallel_multiinstance_tasks
This commit is contained in:
commit
150117587e
3
Pipfile
3
Pipfile
|
@ -24,6 +24,8 @@ flask-restful = "*"
|
|||
gunicorn = "*"
|
||||
httpretty = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
markdown = "*"
|
||||
marshmallow = "*"
|
||||
marshmallow-enum = "*"
|
||||
marshmallow-sqlalchemy = "*"
|
||||
|
@ -42,7 +44,6 @@ webtest = "*"
|
|||
werkzeug = "*"
|
||||
xlrd = "*"
|
||||
xlsxwriter = "*"
|
||||
lxml = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d82c06e080dbdd4c9da4e308d29ebefd9ef41be7a15caa72c6d6f9b7007d8910"
|
||||
"sha256": "45ac71a0a66c2f55518be6fbc93a1b76e6a53ad3c7a557c3cb371d07781698b6"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -35,7 +35,6 @@
|
|||
"sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b",
|
||||
"sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"aniso8601": {
|
||||
|
@ -50,7 +49,6 @@
|
|||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"babel": {
|
||||
|
@ -58,7 +56,6 @@
|
|||
"sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
|
||||
"sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"bcrypt": {
|
||||
|
@ -82,7 +79,6 @@
|
|||
"sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
|
||||
"sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.1.7"
|
||||
},
|
||||
"beautifulsoup4": {
|
||||
|
@ -111,7 +107,6 @@
|
|||
"sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916",
|
||||
"sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.4.6"
|
||||
},
|
||||
"certifi": {
|
||||
|
@ -166,7 +161,6 @@
|
|||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"clickclick": {
|
||||
|
@ -188,7 +182,6 @@
|
|||
"sha256:2ca44140ee259b5e3d8aaf47c79c36a7ab0d5e94d70bd4105c03ede7a20ea5a1",
|
||||
"sha256:cffc044844040c7ce04e9acd1838b5f2e5fa3170182f6fda4d2ea8b0099dbadd"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"connexion": {
|
||||
|
@ -244,7 +237,6 @@
|
|||
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
|
||||
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.16"
|
||||
},
|
||||
"docxtpl": {
|
||||
|
@ -327,14 +319,12 @@
|
|||
"sha256:0b656fbf87c5f24109d859bafa791d29751fabbda2302b606881ae5485b557a5",
|
||||
"sha256:fcfe6df52cd2ed8a63008ca36b86a51fa7a4b70cef1c39e5625f722fca32308e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.3"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"gunicorn": {
|
||||
|
@ -357,7 +347,6 @@
|
|||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.9"
|
||||
},
|
||||
"imagesize": {
|
||||
|
@ -365,7 +354,6 @@
|
|||
"sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
|
||||
"sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.2.0"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
|
@ -381,7 +369,6 @@
|
|||
"sha256:88b101b2668a1d81d6d72d4c2018e53bc6c7fc544c987849da1c7f77545c3bc9",
|
||||
"sha256:f576e85132d34f5bf7df5183c2c6f94cfb32e528f53065345cf71329ba0b8924"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"itsdangerous": {
|
||||
|
@ -389,7 +376,6 @@
|
|||
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jdcal": {
|
||||
|
@ -404,7 +390,6 @@
|
|||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"jsonschema": {
|
||||
|
@ -419,16 +404,11 @@
|
|||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
||||
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.6.11"
|
||||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
"sha256:17f04298b70bf7ecaa5db8a7d8622b5a962ef7fc2b245b2eea705ac1c24338c0",
|
||||
"sha256:298769ab0232b3a3efa1e84881096c24526fe37911c83a11285f222fe4975efd",
|
||||
"sha256:4fd2db72d0412cc16ee86be01332095e86e361329c3579b314231eb2e56c7871",
|
||||
"sha256:52ab557b3c4908db4a90bea16731aa714b1b54e039b54fd4c4b83994c6c48c0c",
|
||||
"sha256:53aaae5bf14f3827c69600ddf4d61b88f49c055bb93060e9702c5bafd206c744",
|
||||
"sha256:81df4ac8b6df10fb1f05b17c18d0cb8c4c344d5a03083c382824960ed959cf5b"
|
||||
],
|
||||
"index": "pypi",
|
||||
|
@ -472,9 +452,16 @@
|
|||
"sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27",
|
||||
"sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.3"
|
||||
},
|
||||
"markdown": {
|
||||
"hashes": [
|
||||
"sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17",
|
||||
"sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.2"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
|
@ -511,7 +498,6 @@
|
|||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"marshmallow": {
|
||||
|
@ -567,7 +553,6 @@
|
|||
"sha256:df1889701e2dfd8ba4dc9b1a010f0a60950077fb5242bb92c8b5c7f1a6f2668a",
|
||||
"sha256:fa1fe75b4a9e18b66ae7f0b122543c42debcf800aaafa0212aaff3ad273c2596"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.19.0"
|
||||
},
|
||||
"openapi-spec-validator": {
|
||||
|
@ -590,7 +575,6 @@
|
|||
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.4"
|
||||
},
|
||||
"pandas": {
|
||||
|
@ -653,19 +637,8 @@
|
|||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
|
||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
|
||||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
|
@ -674,7 +647,6 @@
|
|||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.20"
|
||||
},
|
||||
"pygments": {
|
||||
|
@ -682,7 +654,6 @@
|
|||
"sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
|
||||
"sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.6.1"
|
||||
},
|
||||
"pyjwt": {
|
||||
|
@ -698,7 +669,6 @@
|
|||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pyrsistent": {
|
||||
|
@ -725,9 +695,7 @@
|
|||
"hashes": [
|
||||
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
|
||||
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
|
||||
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8",
|
||||
"sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77",
|
||||
"sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"
|
||||
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
|
||||
],
|
||||
"version": "==1.0.4"
|
||||
},
|
||||
|
@ -841,7 +809,6 @@
|
|||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
|
@ -856,7 +823,6 @@
|
|||
"sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55",
|
||||
"sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"sphinx": {
|
||||
|
@ -872,7 +838,6 @@
|
|||
"sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
|
||||
"sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"sphinxcontrib-devhelp": {
|
||||
|
@ -880,7 +845,6 @@
|
|||
"sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
|
||||
"sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"sphinxcontrib-htmlhelp": {
|
||||
|
@ -888,7 +852,6 @@
|
|||
"sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f",
|
||||
"sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.3"
|
||||
},
|
||||
"sphinxcontrib-jsmath": {
|
||||
|
@ -896,7 +859,6 @@
|
|||
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
|
||||
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"sphinxcontrib-qthelp": {
|
||||
|
@ -904,7 +866,6 @@
|
|||
"sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
|
||||
"sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.3"
|
||||
},
|
||||
"sphinxcontrib-serializinghtml": {
|
||||
|
@ -912,47 +873,45 @@
|
|||
"sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc",
|
||||
"sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.1.4"
|
||||
},
|
||||
"spiffworkflow": {
|
||||
"editable": true,
|
||||
"git": "https://github.com/sartography/SpiffWorkflow.git",
|
||||
"ref": "8ab8d98792ac46e0bac5b1b35a59ddfe28aa9760"
|
||||
"ref": "bd2af4ef61ad3adaf193635bbb21729d067f033b"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:128bc917ed20d78143a45024455ff0aed7d3b96772eba13d5dbaf9cc57e5c41b",
|
||||
"sha256:156a27548ba4e1fed944ff9fcdc150633e61d350d673ae7baaf6c25c04ac1f71",
|
||||
"sha256:27e2efc8f77661c9af2681755974205e7462f1ae126f498f4fe12a8b24761d15",
|
||||
"sha256:2a12f8be25b9ea3d1d5b165202181f2b7da4b3395289000284e5bb86154ce87c",
|
||||
"sha256:31c043d5211aa0e0773821fcc318eb5cbe2ec916dfbc4c6eea0c5188971988eb",
|
||||
"sha256:65eb3b03229f684af0cf0ad3bcc771970c1260a82a791a8d07bffb63d8c95bcc",
|
||||
"sha256:6cd157ce74a911325e164441ff2d9b4e244659a25b3146310518d83202f15f7a",
|
||||
"sha256:703c002277f0fbc3c04d0ae4989a174753a7554b2963c584ce2ec0cddcf2bc53",
|
||||
"sha256:869bbb637de58ab0a912b7f20e9192132f9fbc47fc6b5111cd1e0f6cdf5cf9b0",
|
||||
"sha256:8a0e0cd21da047ea10267c37caf12add400a92f0620c8bc09e4a6531a765d6d7",
|
||||
"sha256:8d01e949a5d22e5c4800d59b50617c56125fc187fbeb8fa423e99858546de616",
|
||||
"sha256:925b4fe5e7c03ed76912b75a9a41dfd682d59c0be43bce88d3b27f7f5ba028fb",
|
||||
"sha256:9cb1819008f0225a7c066cac8bb0cf90847b2c4a6eb9ebb7431dbd00c56c06c5",
|
||||
"sha256:a87d496884f40c94c85a647c385f4fd5887941d2609f71043e2b73f2436d9c65",
|
||||
"sha256:a9030cd30caf848a13a192c5e45367e3c6f363726569a56e75dc1151ee26d859",
|
||||
"sha256:a9e75e49a0f1583eee0ce93270232b8e7bb4b1edc89cc70b07600d525aef4f43",
|
||||
"sha256:b50f45d0e82b4562f59f0e0ca511f65e412f2a97d790eea5f60e34e5f1aabc9a",
|
||||
"sha256:b7878e59ec31f12d54b3797689402ee3b5cfcb5598f2ebf26491732758751908",
|
||||
"sha256:ce1ddaadee913543ff0154021d31b134551f63428065168e756d90bdc4c686f5",
|
||||
"sha256:ce2646e4c0807f3461be0653502bb48c6e91a5171d6e450367082c79e12868bf",
|
||||
"sha256:ce6c3d18b2a8ce364013d47b9cad71db815df31d55918403f8db7d890c9d07ae",
|
||||
"sha256:e4e2664232005bd306f878b0f167a31f944a07c4de0152c444f8c61bbe3cfb38",
|
||||
"sha256:e8aa395482728de8bdcca9cc0faf3765ab483e81e01923aaa736b42f0294f570",
|
||||
"sha256:eb4fcf7105bf071c71068c6eee47499ab8d4b8f5a11fc35147c934f0faa60f23",
|
||||
"sha256:ed375a79f06cad285166e5be74745df1ed6845c5624aafadec4b7a29c25866ef",
|
||||
"sha256:f35248f7e0d63b234a109dd72fbfb4b5cb6cb6840b221d0df0ecbf54ab087654",
|
||||
"sha256:f502ef245c492b391e0e23e94cba030ab91722dcc56963c85bfd7f3441ea2bbe",
|
||||
"sha256:fe01bac7226499aedf472c62fa3b85b2c619365f3f14dd222ffe4f3aa91e5f98"
|
||||
"sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e",
|
||||
"sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772",
|
||||
"sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7",
|
||||
"sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf",
|
||||
"sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98",
|
||||
"sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864",
|
||||
"sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9",
|
||||
"sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1",
|
||||
"sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd",
|
||||
"sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4",
|
||||
"sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1",
|
||||
"sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c",
|
||||
"sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8",
|
||||
"sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e",
|
||||
"sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce",
|
||||
"sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1",
|
||||
"sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5",
|
||||
"sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe",
|
||||
"sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413",
|
||||
"sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3",
|
||||
"sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284",
|
||||
"sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1",
|
||||
"sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7",
|
||||
"sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299",
|
||||
"sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33",
|
||||
"sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d",
|
||||
"sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274",
|
||||
"sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.3.17"
|
||||
"version": "==1.3.18"
|
||||
},
|
||||
"swagger-ui-bundle": {
|
||||
"hashes": [
|
||||
|
@ -968,7 +927,6 @@
|
|||
"sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
|
||||
"sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.25.9"
|
||||
},
|
||||
"vine": {
|
||||
|
@ -976,7 +934,6 @@
|
|||
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
|
||||
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"waitress": {
|
||||
|
@ -984,7 +941,6 @@
|
|||
"sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261",
|
||||
"sha256:3d633e78149eb83b60a07dfabb35579c29aac2d24bb803c18b26fb2ab1a584db"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"webob": {
|
||||
|
@ -992,7 +948,6 @@
|
|||
"sha256:a3c89a8e9ba0aeb17382836cdb73c516d0ecf6630ec40ec28288f3ed459ce87b",
|
||||
"sha256:aa3a917ed752ba3e0b242234b2a373f9c4e2a75d35291dcbe977649bd21fd108"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.8.6"
|
||||
},
|
||||
"webtest": {
|
||||
|
@ -1039,7 +994,6 @@
|
|||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.0"
|
||||
}
|
||||
},
|
||||
|
@ -1049,7 +1003,6 @@
|
|||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"coverage": {
|
||||
|
@ -1102,7 +1055,6 @@
|
|||
"sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5",
|
||||
"sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==8.4.0"
|
||||
},
|
||||
"packaging": {
|
||||
|
@ -1110,7 +1062,6 @@
|
|||
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.4"
|
||||
},
|
||||
"pbr": {
|
||||
|
@ -1126,7 +1077,6 @@
|
|||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
|
@ -1134,7 +1084,6 @@
|
|||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
|
@ -1142,7 +1091,6 @@
|
|||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
|
@ -1158,7 +1106,6 @@
|
|||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"wcwidth": {
|
||||
|
@ -1173,7 +1120,6 @@
|
|||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu").strip('/') # No
|
|||
LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=1))
|
||||
|
||||
# Email configuration
|
||||
DEFAULT_SENDER = 'askresearch@virginia.edu'
|
||||
FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com']
|
||||
MAIL_DEBUG = environ.get('MAIL_DEBUG', default=True)
|
||||
MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io')
|
||||
|
|
|
@ -34,6 +34,9 @@ db = SQLAlchemy(app)
|
|||
session = db.session
|
||||
""":type: sqlalchemy.orm.Session"""
|
||||
|
||||
# Mail settings
|
||||
mail = Mail(app)
|
||||
|
||||
migrate = Migrate(app, db)
|
||||
ma = Marshmallow(app)
|
||||
|
||||
|
@ -58,8 +61,6 @@ if app.config['ENABLE_SENTRY']:
|
|||
# Jinja environment definition, used to render mail templates
|
||||
template_dir = os.getcwd() + '/crc/static/templates/mails'
|
||||
env = Environment(loader=FileSystemLoader(template_dir))
|
||||
# Mail settings
|
||||
mail = Mail(app)
|
||||
|
||||
print('=== USING THESE CONFIG SETTINGS: ===')
|
||||
print('APPLICATION_ROOT = ', app.config['APPLICATION_ROOT'])
|
||||
|
|
15
crc/api.yml
15
crc/api.yml
|
@ -923,6 +923,21 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
/health_attesting:
|
||||
get:
|
||||
operationId: crc.api.approval.get_health_attesting_csv
|
||||
summary: Returns a CSV file with health attesting records
|
||||
tags:
|
||||
- Approvals
|
||||
responses:
|
||||
'200':
|
||||
description: A CSV file
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Approval"
|
||||
components:
|
||||
securitySchemes:
|
||||
jwt:
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import csv
|
||||
import io
|
||||
import json
|
||||
import pickle
|
||||
from base64 import b64decode
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g
|
||||
from flask import g, make_response
|
||||
|
||||
from crc import db, session
|
||||
from crc.api.common import ApiError
|
||||
|
@ -88,71 +90,25 @@ def get_approvals_for_study(study_id=None):
|
|||
return results
|
||||
|
||||
|
||||
def get_health_attesting_csv():
|
||||
records = ApprovalService.get_health_attesting_records()
|
||||
si = io.StringIO()
|
||||
cw = csv.writer(si)
|
||||
cw.writerows(records)
|
||||
output = make_response(si.getvalue())
|
||||
output.headers["Content-Disposition"] = "attachment; filename=health_attesting.csv"
|
||||
output.headers["Content-type"] = "text/csv"
|
||||
return output
|
||||
|
||||
|
||||
# ----- Begin descent into madness ---- #
|
||||
def get_csv():
|
||||
"""A damn lie, it's a json file. A huge bit of a one-off for RRT, but 3 weeks of midnight work can convince a
|
||||
man to do just about anything"""
|
||||
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
|
||||
output = []
|
||||
errors = []
|
||||
for approval in approvals:
|
||||
try:
|
||||
if approval.status != ApprovalStatus.APPROVED.value:
|
||||
continue
|
||||
for related_approval in approval.related_approvals:
|
||||
if related_approval.status != ApprovalStatus.APPROVED.value:
|
||||
continue
|
||||
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == approval.workflow_id).first()
|
||||
data = json.loads(workflow.bpmn_workflow_json)
|
||||
last_task = find_task(data['last_task']['__uuid__'], data['task_tree'])
|
||||
personnel = extract_value(last_task, 'personnel')
|
||||
training_val = extract_value(last_task, 'RequiredTraining')
|
||||
pi_supervisor = extract_value(last_task, 'PISupervisor')['value']
|
||||
review_complete = 'AllRequiredTraining' in training_val
|
||||
pi_uid = workflow.study.primary_investigator_id
|
||||
pi_details = LdapService.user_info(pi_uid)
|
||||
details = []
|
||||
details.append(pi_details)
|
||||
for person in personnel:
|
||||
uid = person['PersonnelComputingID']['value']
|
||||
details.append(LdapService.user_info(uid))
|
||||
content = ApprovalService.get_not_really_csv_content()
|
||||
|
||||
for person in details:
|
||||
record = {
|
||||
"study_id": approval.study_id,
|
||||
"pi_uid": pi_details.uid,
|
||||
"pi": pi_details.display_name,
|
||||
"name": person.display_name,
|
||||
"uid": person.uid,
|
||||
"email": person.email_address,
|
||||
"supervisor": "",
|
||||
"review_complete": review_complete,
|
||||
}
|
||||
# We only know the PI's supervisor.
|
||||
if person.uid == pi_details.uid:
|
||||
record["supervisor"] = pi_supervisor
|
||||
return content
|
||||
|
||||
output.append(record)
|
||||
|
||||
except Exception as e:
|
||||
errors.append("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
|
||||
return {"results": output, "errors": errors }
|
||||
|
||||
|
||||
def extract_value(task, key):
|
||||
if key in task['data']:
|
||||
return pickle.loads(b64decode(task['data'][key]['__bytes__']))
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def find_task(uuid, task):
|
||||
if task['id']['__uuid__'] == uuid:
|
||||
return task
|
||||
for child in task['children']:
|
||||
task = find_task(uuid, child)
|
||||
if task:
|
||||
return task
|
||||
# ----- come back to the world of the living ---- #
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from crc.services.mails import send_test_email
|
|||
|
||||
def render_markdown(data, template):
|
||||
"""
|
||||
Provides a quick way to very that a Jinja markdown template will work properly on a given json
|
||||
Provides a quick way to very that a Jinja markdown template will work properly on a given json
|
||||
data structure. Useful for folks that are building these markdown templates.
|
||||
"""
|
||||
try:
|
||||
|
@ -65,4 +65,4 @@ def send_email(address):
|
|||
"""Just sends a quick test email to assure the system is working."""
|
||||
if not address:
|
||||
address = "dan@sartography.com"
|
||||
return send_test_email(address, [address])
|
||||
return send_test_email(address, [address])
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
|
||||
from marshmallow import EXCLUDE
|
||||
from sqlalchemy import func
|
||||
|
||||
from crc import db
|
||||
from crc.models.study import StudyModel
|
||||
|
||||
|
||||
class EmailModel(db.Model):
|
||||
__tablename__ = 'email'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
subject = db.Column(db.String)
|
||||
sender = db.Column(db.String)
|
||||
recipients = db.Column(db.String)
|
||||
content = db.Column(db.String)
|
||||
content_html = db.Column(db.String)
|
||||
study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=True)
|
||||
study = db.relationship(StudyModel)
|
|
@ -29,6 +29,9 @@ class LdapModel(db.Model):
|
|||
affiliation=", ".join(entry.uvaPersonIAMAffiliation),
|
||||
sponsor_type=", ".join(entry.uvaPersonSponsoredType))
|
||||
|
||||
def proper_name(self):
|
||||
return f'{self.display_name} - ({self.uid})'
|
||||
|
||||
|
||||
class LdapSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import markdown
|
||||
from jinja2 import Template
|
||||
|
||||
from crc import app
|
||||
from crc.api.common import ApiError
|
||||
from crc.scripts.script import Script
|
||||
from crc.services.ldap_service import LdapService
|
||||
from crc.services.mails import send_mail
|
||||
|
||||
|
||||
class Email(Script):
|
||||
"""This Script allows to be introduced as part of a workflow and called from there, specifying
|
||||
recipients and content """
|
||||
|
||||
def get_description(self):
|
||||
return """
|
||||
Creates an email, using the provided arguments (a list of UIDs)"
|
||||
Each argument will be used to look up personal information needed for
|
||||
the email creation.
|
||||
|
||||
Example:
|
||||
Email Subject ApprvlApprvr1 PIComputingID
|
||||
"""
|
||||
|
||||
def do_task_validate_only(self, task, *args, **kwargs):
|
||||
self.get_subject(task, args)
|
||||
self.get_users_info(task, args)
|
||||
self.get_content(task)
|
||||
|
||||
def do_task(self, task, *args, **kwargs):
|
||||
args = [arg for arg in args if type(arg) == str]
|
||||
subject = self.get_subject(task, args)
|
||||
recipients = self.get_users_info(task, args)
|
||||
content, content_html = self.get_content(task)
|
||||
if recipients:
|
||||
send_mail(
|
||||
subject=subject,
|
||||
sender=app.config['DEFAULT_SENDER'],
|
||||
recipients=recipients,
|
||||
content=content,
|
||||
content_html=content_html
|
||||
)
|
||||
|
||||
def get_users_info(self, task, args):
|
||||
if len(args) < 1:
|
||||
raise ApiError(code="missing_argument",
|
||||
message="Email script requires at least one argument. The "
|
||||
"name of the variable in the task data that contains user"
|
||||
"id to process. Multiple arguments are accepted.")
|
||||
emails = []
|
||||
for arg in args:
|
||||
try:
|
||||
uid = task.workflow.script_engine.evaluate_expression(task, arg)
|
||||
except Exception as e:
|
||||
app.logger.error(f'Workflow engines could not parse {arg}')
|
||||
app.logger.error(str(e))
|
||||
continue
|
||||
user_info = LdapService.user_info(uid)
|
||||
email = user_info.email_address
|
||||
emails.append(user_info.email_address)
|
||||
if not isinstance(email, str):
|
||||
raise ApiError(code="invalid_argument",
|
||||
message="The Email script requires at least 1 UID argument. The "
|
||||
"name of the variable in the task data that contains subject and"
|
||||
" user ids to process. This must point to an array or a string, but "
|
||||
"it currently points to a %s " % emails.__class__.__name__)
|
||||
|
||||
return emails
|
||||
|
||||
def get_subject(self, task, args):
|
||||
if len(args) < 1:
|
||||
raise ApiError(code="missing_argument",
|
||||
message="Email script requires at least one subject argument. The "
|
||||
"name of the variable in the task data that contains subject"
|
||||
" to process. Multiple arguments are accepted.")
|
||||
subject = args[0]
|
||||
if not isinstance(subject, str):
|
||||
raise ApiError(code="invalid_argument",
|
||||
message="The Email script requires 1 argument. The "
|
||||
"the name of the variable in the task data that contains user"
|
||||
"ids to process. This must point to an array or a string, but "
|
||||
"it currently points to a %s " % subject.__class__.__name__)
|
||||
|
||||
return subject
|
||||
|
||||
def get_content(self, task):
|
||||
content = task.task_spec.documentation
|
||||
template = Template(content)
|
||||
rendered = template.render(task.data)
|
||||
rendered_markdown = markdown.markdown(rendered).replace('\n', '<br>')
|
||||
return rendered, rendered_markdown
|
|
@ -5,7 +5,7 @@ from crc.scripts.script import Script
|
|||
|
||||
class FactService(Script):
|
||||
def get_description(self):
|
||||
return """Just your basic class that can pull in data from a few api endpoints and
|
||||
return """Just your basic class that can pull in data from a few api endpoints and
|
||||
do a basic task."""
|
||||
|
||||
def get_cat(self):
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
from datetime import datetime
|
||||
import json
|
||||
import pickle
|
||||
from base64 import b64decode
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy import desc, func
|
||||
|
||||
from crc import app, db, session
|
||||
from crc.api.common import ApiError
|
||||
|
@ -109,16 +112,129 @@ class ApprovalService(object):
|
|||
db_approvals = query.all()
|
||||
return [Approval.from_model(approval_model) for approval_model in db_approvals]
|
||||
|
||||
@staticmethod
|
||||
def get_approval_details(approval):
|
||||
"""Returns a list of packed approval details, obtained from
|
||||
the task data sent during the workflow """
|
||||
def extract_value(task, key):
|
||||
if key in task['data']:
|
||||
return pickle.loads(b64decode(task['data'][key]['__bytes__']))
|
||||
else:
|
||||
return ""
|
||||
|
||||
def find_task(uuid, task):
|
||||
if task['id']['__uuid__'] == uuid:
|
||||
return task
|
||||
for child in task['children']:
|
||||
task = find_task(uuid, child)
|
||||
if task:
|
||||
return task
|
||||
|
||||
if approval.status != ApprovalStatus.APPROVED.value:
|
||||
return {}
|
||||
for related_approval in approval.related_approvals:
|
||||
if related_approval.status != ApprovalStatus.APPROVED.value:
|
||||
continue
|
||||
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == approval.workflow_id).first()
|
||||
data = json.loads(workflow.bpmn_workflow_json)
|
||||
last_task = find_task(data['last_task']['__uuid__'], data['task_tree'])
|
||||
personnel = extract_value(last_task, 'personnel')
|
||||
training_val = extract_value(last_task, 'RequiredTraining')
|
||||
pi_supervisor = extract_value(last_task, 'PISupervisor')['value']
|
||||
review_complete = 'AllRequiredTraining' in training_val
|
||||
pi_uid = workflow.study.primary_investigator_id
|
||||
pi_details = LdapService.user_info(pi_uid)
|
||||
details = {
|
||||
'Supervisor': pi_supervisor,
|
||||
'PI_Details': pi_details,
|
||||
'Review': review_complete
|
||||
}
|
||||
details['person_details'] = []
|
||||
details['person_details'].append(pi_details)
|
||||
for person in personnel:
|
||||
uid = person['PersonnelComputingID']['value']
|
||||
details['person_details'].append(LdapService.user_info(uid))
|
||||
|
||||
return details
|
||||
|
||||
@staticmethod
|
||||
def get_health_attesting_records():
|
||||
"""Return a list with prepared information related to all approvals """
|
||||
|
||||
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
|
||||
|
||||
health_attesting_rows = [
|
||||
['university_computing_id',
|
||||
'last_name',
|
||||
'first_name',
|
||||
'department',
|
||||
'job_title',
|
||||
'supervisor_university_computing_id']
|
||||
]
|
||||
|
||||
for approval in approvals:
|
||||
try:
|
||||
details = ApprovalService.get_approval_details(approval)
|
||||
if not details:
|
||||
continue
|
||||
|
||||
for person in details['person_details']:
|
||||
first_name = person.given_name
|
||||
last_name = person.display_name.replace(first_name, '').strip()
|
||||
record = [
|
||||
person.uid,
|
||||
last_name,
|
||||
first_name,
|
||||
'',
|
||||
'Academic Researcher',
|
||||
details['Supervisor'] if person.uid == details['person_details'][0].uid else 'askresearch'
|
||||
]
|
||||
|
||||
if record not in health_attesting_rows:
|
||||
health_attesting_rows.append(record)
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
|
||||
|
||||
return health_attesting_rows
|
||||
|
||||
@staticmethod
|
||||
def get_not_really_csv_content():
|
||||
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
|
||||
output = []
|
||||
errors = []
|
||||
for approval in approvals:
|
||||
try:
|
||||
details = ApprovalService.get_approval_details(approval)
|
||||
|
||||
for person in details['person_details']:
|
||||
record = {
|
||||
"study_id": approval.study_id,
|
||||
"pi_uid": details['PI_Details'].uid,
|
||||
"pi": details['PI_Details'].display_name,
|
||||
"name": person.display_name,
|
||||
"uid": person.uid,
|
||||
"email": person.email_address,
|
||||
"supervisor": details['Supervisor'] if person.uid == details['person_details'][0].uid else "",
|
||||
"review_complete": details['Review'],
|
||||
}
|
||||
|
||||
output.append(record)
|
||||
|
||||
except Exception as e:
|
||||
errors.append("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
|
||||
return {"results": output, "errors": errors }
|
||||
|
||||
@staticmethod
|
||||
def update_approval(approval_id, approver_uid):
|
||||
"""Update a specific approval"""
|
||||
"""Update a specific approval
|
||||
NOTE: Actual update happens in the API layer, this
|
||||
funtion is currently in charge of only sending
|
||||
corresponding emails
|
||||
"""
|
||||
db_approval = session.query(ApprovalModel).get(approval_id)
|
||||
status = db_approval.status
|
||||
if db_approval:
|
||||
# db_approval.status = status
|
||||
# session.add(db_approval)
|
||||
# session.commit()
|
||||
if status == ApprovalStatus.APPROVED.value:
|
||||
# second_approval = ApprovalModel().query.filter_by(
|
||||
# study_id=db_approval.study_id, workflow_id=db_approval.workflow_id,
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
from datetime import datetime
|
||||
from flask_mail import Message
|
||||
from sqlalchemy import desc
|
||||
|
||||
from crc import app, db, mail, session
|
||||
from crc.api.common import ApiError
|
||||
|
||||
from crc.models.study import StudyModel
|
||||
from crc.models.email import EmailModel
|
||||
|
||||
|
||||
class EmailService(object):
|
||||
"""Provides common tools for working with an Email"""
|
||||
|
||||
@staticmethod
|
||||
def add_email(subject, sender, recipients, content, content_html, study_id):
|
||||
"""We will receive all data related to an email and store it"""
|
||||
|
||||
# Find corresponding study - if any
|
||||
study = None
|
||||
if type(study_id) == int:
|
||||
study = db.session.query(StudyModel).get(study_id)
|
||||
|
||||
# Create EmailModel
|
||||
email_model = EmailModel(subject=subject, sender=sender, recipients=str(recipients),
|
||||
content=content, content_html=content_html, study=study)
|
||||
|
||||
# Send mail
|
||||
try:
|
||||
msg = Message(subject,
|
||||
sender=sender,
|
||||
recipients=recipients)
|
||||
|
||||
msg.body = content
|
||||
msg.html = content_html
|
||||
|
||||
mail.send(msg)
|
||||
except Exception as e:
|
||||
app.logger.error(str(e))
|
||||
|
||||
db.session.add(email_model)
|
||||
db.session.commit()
|
|
@ -58,7 +58,7 @@ class FileService(object):
|
|||
"irb_docunents.xslx reference file. This code is not found in that file '%s'" % irb_doc_code)
|
||||
|
||||
"""Assure this is unique to the workflow, task, and document code AND the Name
|
||||
Because we will allow users to upload multiple files for the same form field
|
||||
Because we will allow users to upload multiple files for the same form field
|
||||
in some cases """
|
||||
file_model = session.query(FileModel)\
|
||||
.filter(FileModel.workflow_id == workflow_id)\
|
||||
|
|
|
@ -3,13 +3,15 @@ import os
|
|||
from flask import render_template, render_template_string
|
||||
from flask_mail import Message
|
||||
|
||||
from crc.services.email_service import EmailService
|
||||
|
||||
|
||||
# TODO: Extract common mailing code into its own function
|
||||
def send_test_email(sender, recipients):
|
||||
try:
|
||||
msg = Message('Research Ramp-up Plan test',
|
||||
sender=sender,
|
||||
recipients=recipients)
|
||||
recipients=recipients,
|
||||
bcc=['rrt_emails@googlegroups.com'])
|
||||
from crc import env, mail
|
||||
template = env.get_template('ramp_up_approval_request_first_review.txt')
|
||||
template_vars = {'primary_investigator': "test"}
|
||||
|
@ -20,109 +22,84 @@ def send_test_email(sender, recipients):
|
|||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
|
||||
def send_mail(subject, sender, recipients, content, content_html, study_id=None):
|
||||
EmailService.add_email(subject=subject, sender=sender, recipients=recipients,
|
||||
content=content, content_html=content_html, study_id=study_id)
|
||||
|
||||
def send_ramp_up_submission_email(sender, recipients, approver_1, approver_2=None):
|
||||
try:
|
||||
msg = Message('Research Ramp-up Plan Submitted',
|
||||
sender=sender,
|
||||
recipients=recipients,
|
||||
bcc=['rrt_emails@googlegroups.com'])
|
||||
from crc import env, mail
|
||||
template = env.get_template('ramp_up_submission.txt')
|
||||
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
|
||||
msg.body = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_submission.html')
|
||||
msg.html = template.render(template_vars)
|
||||
from crc import env
|
||||
subject = 'Research Ramp-up Plan Submitted'
|
||||
|
||||
mail.send(msg)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
template = env.get_template('ramp_up_submission.txt')
|
||||
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
|
||||
content = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_submission.html')
|
||||
content_html = template.render(template_vars)
|
||||
|
||||
result = send_mail(subject, sender, recipients, content, content_html)
|
||||
return result
|
||||
|
||||
def send_ramp_up_approval_request_email(sender, recipients, primary_investigator):
|
||||
try:
|
||||
msg = Message('Research Ramp-up Plan Approval Request',
|
||||
sender=sender,
|
||||
recipients=recipients,
|
||||
bcc=['rrt_emails@googlegroups.com'])
|
||||
from crc import env, mail
|
||||
template = env.get_template('ramp_up_approval_request.txt')
|
||||
template_vars = {'primary_investigator': primary_investigator}
|
||||
msg.body = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_approval_request.html')
|
||||
msg.html = template.render(template_vars)
|
||||
from crc import env
|
||||
subject = 'Research Ramp-up Plan Approval Request'
|
||||
|
||||
mail.send(msg)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
template = env.get_template('ramp_up_approval_request.txt')
|
||||
template_vars = {'primary_investigator': primary_investigator}
|
||||
content = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_approval_request.html')
|
||||
content_html = template.render(template_vars)
|
||||
|
||||
result = send_mail(subject, sender, recipients, content, content_html)
|
||||
return result
|
||||
|
||||
def send_ramp_up_approval_request_first_review_email(sender, recipients, primary_investigator):
|
||||
try:
|
||||
msg = Message('Research Ramp-up Plan Approval Request',
|
||||
sender=sender,
|
||||
recipients=recipients,
|
||||
bcc=['rrt_emails@googlegroups.com'])
|
||||
from crc import env, mail
|
||||
template = env.get_template('ramp_up_approval_request_first_review.txt')
|
||||
template_vars = {'primary_investigator': primary_investigator}
|
||||
msg.body = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_approval_request_first_review.html')
|
||||
msg.html = template.render(template_vars)
|
||||
from crc import env
|
||||
subject = 'Research Ramp-up Plan Approval Request'
|
||||
|
||||
mail.send(msg)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
template = env.get_template('ramp_up_approval_request_first_review.txt')
|
||||
template_vars = {'primary_investigator': primary_investigator}
|
||||
content = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_approval_request_first_review.html')
|
||||
content_html = template.render(template_vars)
|
||||
|
||||
result = send_mail(subject, sender, recipients, content, content_html)
|
||||
return result
|
||||
|
||||
def send_ramp_up_approved_email(sender, recipients, approver_1, approver_2=None):
|
||||
try:
|
||||
msg = Message('Research Ramp-up Plan Approved',
|
||||
sender=sender,
|
||||
recipients=recipients,
|
||||
bcc=['rrt_emails@googlegroups.com'])
|
||||
from crc import env
|
||||
subject = 'Research Ramp-up Plan Approved'
|
||||
|
||||
from crc import env, mail
|
||||
template = env.get_template('ramp_up_approved.txt')
|
||||
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
|
||||
msg.body = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_approved.html')
|
||||
msg.html = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_approved.txt')
|
||||
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
|
||||
content = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_approved.html')
|
||||
content_html = template.render(template_vars)
|
||||
|
||||
mail.send(msg)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
result = send_mail(subject, sender, recipients, content, content_html)
|
||||
return result
|
||||
|
||||
def send_ramp_up_denied_email(sender, recipients, approver):
|
||||
try:
|
||||
msg = Message('Research Ramp-up Plan Denied',
|
||||
sender=sender,
|
||||
recipients=recipients,
|
||||
bcc=['rrt_emails@googlegroups.com'])
|
||||
from crc import env
|
||||
subject = 'Research Ramp-up Plan Denied'
|
||||
|
||||
from crc import env, mail
|
||||
template = env.get_template('ramp_up_denied.txt')
|
||||
template_vars = {'approver': approver}
|
||||
msg.body = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_denied.html')
|
||||
msg.html = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_denied.txt')
|
||||
template_vars = {'approver': approver}
|
||||
content = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_denied.html')
|
||||
content_html = template.render(template_vars)
|
||||
|
||||
mail.send(msg)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
result = send_mail(subject, sender, recipients, content, content_html)
|
||||
return result
|
||||
|
||||
def send_ramp_up_denied_email_to_approver(sender, recipients, primary_investigator, approver_2):
|
||||
try:
|
||||
msg = Message('Research Ramp-up Plan Denied',
|
||||
sender=sender,
|
||||
recipients=recipients,
|
||||
bcc=['rrt_emails@googlegroups.com'])
|
||||
from crc import env
|
||||
subject = 'Research Ramp-up Plan Denied'
|
||||
|
||||
from crc import env, mail
|
||||
template = env.get_template('ramp_up_denied_first_approver.txt')
|
||||
template_vars = {'primary_investigator': primary_investigator, 'approver_2': approver_2}
|
||||
msg.body = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_denied_first_approver.html')
|
||||
msg.html = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_denied_first_approver.txt')
|
||||
template_vars = {'primary_investigator': primary_investigator, 'approver_2': approver_2}
|
||||
content = template.render(template_vars)
|
||||
template = env.get_template('ramp_up_denied_first_approver.html')
|
||||
content_html = template.render(template_vars)
|
||||
|
||||
mail.send(msg)
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
result = send_mail(subject, sender, recipients, content, content_html)
|
||||
return result
|
||||
|
|
|
@ -181,8 +181,6 @@ class StudyService(object):
|
|||
documents[code] = doc
|
||||
return documents
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_investigators(study_id):
|
||||
|
||||
|
@ -224,7 +222,6 @@ class StudyService(object):
|
|||
|
||||
return FileModelSchema().dump(file)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def synch_with_protocol_builder_if_enabled(user):
|
||||
"""Assures that the studies we have locally for the given user are
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
from lxml import etree
|
||||
import shlex
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
|
@ -13,7 +14,6 @@ from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser
|
|||
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
|
||||
from SpiffWorkflow.exceptions import WorkflowTaskExecException
|
||||
from SpiffWorkflow.specs import WorkflowSpec
|
||||
from sqlalchemy import desc
|
||||
|
||||
from crc import session
|
||||
from crc.api.common import ApiError
|
||||
|
@ -36,7 +36,9 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
|
|||
|
||||
This allows us to reference custom code from the BPMN diagram.
|
||||
"""
|
||||
commands = script.split(" ")
|
||||
# Shlex splits the whole string while respecting double quoted strings within
|
||||
commands = shlex.split(script)
|
||||
printable_comms = commands
|
||||
path_and_command = commands[0].rsplit(".", 1)
|
||||
if len(path_and_command) == 1:
|
||||
module_name = "crc.scripts." + self.camel_to_snake(path_and_command[0])
|
||||
|
@ -60,7 +62,7 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
|
|||
"does not properly implement the CRC Script class.",
|
||||
task=task)
|
||||
if task.workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY]:
|
||||
"""If this is running a validation, and not a normal process, then we want to
|
||||
"""If this is running a validation, and not a normal process, then we want to
|
||||
mimic running the script, but not make any external calls or database changes."""
|
||||
klass().do_task_validate_only(task, study_id, workflow_id, *commands[1:])
|
||||
else:
|
||||
|
|
|
@ -39,7 +39,7 @@ class WorkflowService(object):
|
|||
the workflow Processor should be hidden behind this service.
|
||||
This will help maintain a structure that avoids circular dependencies.
|
||||
But for now, this contains tools for converting spiff-workflow models into our
|
||||
own API models with additional information and capabilities and
|
||||
own API models with additional information and capabilities and
|
||||
handles the testing of a workflow specification by completing it with
|
||||
random selections, attempting to mimic a front end as much as possible. """
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Your Research Ramp-up Plan has been denied by {{ approver_1 }}. Please return to the Research Ramp-up Plan application and review the comments from {{ approver_1 }} on the home page. Next, open the application and locate the first step where changes are needed. Continue to complete additional steps saving your work along the way. Review your revised Research Ramp-up Plan and res-submit for approval.
|
||||
Your Research Ramp-up Plan has been denied by {{ approver }}. Please return to the Research Ramp-up Plan application and review the comments from {{ approver }} on the home page. Next, open the application and locate the first step where changes are needed. Continue to complete additional steps saving your work along the way. Review your revised Research Ramp-up Plan and res-submit for approval.
|
|
@ -0,0 +1,38 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 5acd138e969c
|
||||
Revises: de30304ff5e6
|
||||
Create Date: 2020-06-24 21:36:15.128632
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5acd138e969c'
|
||||
down_revision = 'de30304ff5e6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('email',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('subject', sa.String(), nullable=True),
|
||||
sa.Column('sender', sa.String(), nullable=True),
|
||||
sa.Column('recipients', sa.String(), nullable=True),
|
||||
sa.Column('content', sa.String(), nullable=True),
|
||||
sa.Column('content_html', sa.String(), nullable=True),
|
||||
sa.Column('study_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('email')
|
||||
# ### end Alembic commands ###
|
|
@ -57,6 +57,32 @@ class TestApprovalsService(BaseTest):
|
|||
self.assertEqual(1, models[0].version)
|
||||
self.assertEqual(2, models[1].version)
|
||||
|
||||
def test_get_health_attesting_records(self):
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
workflow = self.create_workflow('empty_workflow')
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code="AD_CoCAppr")
|
||||
|
||||
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
|
||||
records = ApprovalService.get_health_attesting_records()
|
||||
|
||||
self.assertEqual(len(records), 1)
|
||||
|
||||
def test_get_not_really_csv_content(self):
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
workflow = self.create_workflow('empty_workflow')
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code="AD_CoCAppr")
|
||||
|
||||
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
|
||||
records = ApprovalService.get_not_really_csv_content()
|
||||
|
||||
self.assertEqual(len(records), 2)
|
||||
|
||||
def test_new_approval_sends_proper_emails(self):
|
||||
self.assertEqual(1, 1)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0y2dq4f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||
<bpmn:process id="Process_0tad5ma" name="Set Recipients" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_1synsig</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:endEvent id="Event_0izrcj4">
|
||||
<bpmn:incoming>Flow_1xlrgne</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:scriptTask id="Activity_0s5v97n" name="Email Recipients">
|
||||
<bpmn:documentation># Dear Approver
|
||||
## you have been requested for approval
|
||||
|
||||
|
||||
---
|
||||
New request submitted by {{ PIComputingID }}
|
||||
|
||||
Email content to be delivered to {{ ApprvlApprvr1 }}
|
||||
|
||||
---</bpmn:documentation>
|
||||
<bpmn:incoming>Flow_08n2npe</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1xlrgne</bpmn:outgoing>
|
||||
<bpmn:script>Email "Camunda Email Subject" ApprvlApprvr1 PIComputingID</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:sequenceFlow id="Flow_1synsig" sourceRef="StartEvent_1" targetRef="Activity_1l9vih3" />
|
||||
<bpmn:sequenceFlow id="Flow_1xlrgne" sourceRef="Activity_0s5v97n" targetRef="Event_0izrcj4" />
|
||||
<bpmn:sequenceFlow id="Flow_08n2npe" sourceRef="Activity_1l9vih3" targetRef="Activity_0s5v97n" />
|
||||
<bpmn:userTask id="Activity_1l9vih3" name="Set Recipients">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="ApprvlApprvr1" label="Approver" type="string" />
|
||||
<camunda:formField id="PIComputingID" label="Primary Investigator" type="string" />
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1synsig</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_08n2npe</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0tad5ma">
|
||||
<bpmndi:BPMNEdge id="Flow_08n2npe_di" bpmnElement="Flow_08n2npe">
|
||||
<di:waypoint x="370" y="117" />
|
||||
<di:waypoint x="450" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1xlrgne_di" bpmnElement="Flow_1xlrgne">
|
||||
<di:waypoint x="550" y="117" />
|
||||
<di:waypoint x="662" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1synsig_di" bpmnElement="Flow_1synsig">
|
||||
<di:waypoint x="215" y="117" />
|
||||
<di:waypoint x="270" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_0izrcj4_di" bpmnElement="Event_0izrcj4">
|
||||
<dc:Bounds x="662" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_04imfm6_di" bpmnElement="Activity_0s5v97n">
|
||||
<dc:Bounds x="450" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0xugr62_di" bpmnElement="Activity_1l9vih3">
|
||||
<dc:Bounds x="270" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -175,9 +175,6 @@ Your random fact is:
|
|||
<bpmndi:BPMNShape id="UserTask_186s7tw_di" bpmnElement="Task_User_Select_Type">
|
||||
<dc:Bounds x="270" y="210" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ScriptTask_10keafb_di" bpmnElement="Task_Get_Fact_From_API">
|
||||
<dc:Bounds x="470" y="210" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
|
||||
<dc:Bounds x="692" y="232" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
|
@ -187,6 +184,9 @@ Your random fact is:
|
|||
<bpmndi:BPMNShape id="TextAnnotation_1234e5n_di" bpmnElement="TextAnnotation_1234e5n">
|
||||
<dc:Bounds x="570" y="120" width="100" height="68" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ScriptTask_10keafb_di" bpmnElement="Task_Get_Fact_From_API">
|
||||
<dc:Bounds x="470" y="210" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Association_1cfasjp_di" bpmnElement="Association_1cfasjp">
|
||||
<di:waypoint x="344" y="210" />
|
||||
<di:waypoint x="359" y="184" />
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
from tests.base_test import BaseTest
|
||||
|
||||
from crc.models.email import EmailModel
|
||||
from crc.services.file_service import FileService
|
||||
from crc.scripts.email import Email
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
from crc.api.common import ApiError
|
||||
|
||||
from crc import db, mail
|
||||
|
||||
|
||||
class TestEmailScript(BaseTest):
|
||||
|
||||
def test_do_task(self):
|
||||
workflow = self.create_workflow('email')
|
||||
|
||||
task_data = {
|
||||
'PIComputingID': 'dhf8r',
|
||||
'ApprvlApprvr1': 'lb3dp'
|
||||
}
|
||||
task = self.get_workflow_api(workflow).next_task
|
||||
|
||||
with mail.record_messages() as outbox:
|
||||
|
||||
self.complete_form(workflow, task, task_data)
|
||||
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertEqual(outbox[0].subject, 'Camunda Email Subject')
|
||||
|
||||
# PI is present
|
||||
self.assertIn(task_data['PIComputingID'], outbox[0].body)
|
||||
self.assertIn(task_data['PIComputingID'], outbox[0].html)
|
||||
|
||||
# Approver is present
|
||||
self.assertIn(task_data['ApprvlApprvr1'], outbox[0].body)
|
||||
self.assertIn(task_data['ApprvlApprvr1'], outbox[0].html)
|
||||
|
||||
db_emails = EmailModel.query.count()
|
||||
self.assertEqual(db_emails, 1)
|
|
@ -0,0 +1,34 @@
|
|||
from tests.base_test import BaseTest
|
||||
|
||||
from crc import session
|
||||
from crc.models.approval import ApprovalModel, ApprovalStatus
|
||||
from crc.models.email import EmailModel
|
||||
from crc.services.email_service import EmailService
|
||||
|
||||
|
||||
class TestEmailService(BaseTest):
|
||||
|
||||
def test_add_email(self):
|
||||
self.load_example_data()
|
||||
study = self.create_study()
|
||||
workflow = self.create_workflow('random_fact')
|
||||
|
||||
subject = 'Email Subject'
|
||||
sender = 'sender@sartography.com'
|
||||
recipients = ['recipient@sartography.com', 'back@sartography.com']
|
||||
content = 'Content for this email'
|
||||
content_html = '<p>Hypertext Markup Language content for this email</p>'
|
||||
|
||||
EmailService.add_email(subject=subject, sender=sender, recipients=recipients,
|
||||
content=content, content_html=content_html, study_id=study.id)
|
||||
|
||||
email_model = EmailModel.query.first()
|
||||
|
||||
self.assertEqual(email_model.subject, subject)
|
||||
self.assertEqual(email_model.sender, sender)
|
||||
self.assertEqual(email_model.recipients, str(recipients))
|
||||
self.assertEqual(email_model.content, content)
|
||||
self.assertEqual(email_model.content_html, content_html)
|
||||
self.assertEqual(email_model.study, study)
|
||||
|
||||
# TODO: Create email model without study
|
|
@ -0,0 +1,117 @@
|
|||
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc import mail, session
|
||||
from crc.models.approval import ApprovalModel, ApprovalStatus
|
||||
from crc.models.email import EmailModel
|
||||
from crc.services.mails import (
|
||||
send_ramp_up_submission_email,
|
||||
send_ramp_up_approval_request_email,
|
||||
send_ramp_up_approval_request_first_review_email,
|
||||
send_ramp_up_approved_email,
|
||||
send_ramp_up_denied_email,
|
||||
send_ramp_up_denied_email_to_approver
|
||||
)
|
||||
|
||||
|
||||
class TestMails(BaseTest):
|
||||
|
||||
def setUp(self):
|
||||
"""Initial setup shared by all TestApprovals tests"""
|
||||
self.load_example_data()
|
||||
self.study = self.create_study()
|
||||
self.workflow = self.create_workflow('random_fact')
|
||||
|
||||
self.sender = 'sender@sartography.com'
|
||||
self.recipients = ['recipient@sartography.com']
|
||||
self.primary_investigator = 'Dr. Bartlett'
|
||||
self.approver_1 = 'Max Approver'
|
||||
self.approver_2 = 'Close Reviewer'
|
||||
|
||||
def test_send_ramp_up_submission_email(self):
|
||||
with mail.record_messages() as outbox:
|
||||
|
||||
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Submitted')
|
||||
self.assertIn(self.approver_1, outbox[0].body)
|
||||
self.assertIn(self.approver_1, outbox[0].html)
|
||||
|
||||
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1, self.approver_2)
|
||||
self.assertEqual(len(outbox), 2)
|
||||
self.assertIn(self.approver_1, outbox[1].body)
|
||||
self.assertIn(self.approver_1, outbox[1].html)
|
||||
self.assertIn(self.approver_2, outbox[1].body)
|
||||
self.assertIn(self.approver_2, outbox[1].html)
|
||||
|
||||
db_emails = EmailModel.query.count()
|
||||
self.assertEqual(db_emails, 2)
|
||||
|
||||
def test_send_ramp_up_approval_request_email(self):
|
||||
with mail.record_messages() as outbox:
|
||||
send_ramp_up_approval_request_email(self.sender, self.recipients, self.primary_investigator)
|
||||
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Approval Request')
|
||||
self.assertIn(self.primary_investigator, outbox[0].body)
|
||||
self.assertIn(self.primary_investigator, outbox[0].html)
|
||||
|
||||
db_emails = EmailModel.query.count()
|
||||
self.assertEqual(db_emails, 1)
|
||||
|
||||
def test_send_ramp_up_approval_request_first_review_email(self):
|
||||
with mail.record_messages() as outbox:
|
||||
send_ramp_up_approval_request_first_review_email(
|
||||
self.sender, self.recipients, self.primary_investigator
|
||||
)
|
||||
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Approval Request')
|
||||
self.assertIn(self.primary_investigator, outbox[0].body)
|
||||
self.assertIn(self.primary_investigator, outbox[0].html)
|
||||
|
||||
db_emails = EmailModel.query.count()
|
||||
self.assertEqual(db_emails, 1)
|
||||
|
||||
def test_send_ramp_up_approved_email(self):
|
||||
with mail.record_messages() as outbox:
|
||||
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Approved')
|
||||
self.assertIn(self.approver_1, outbox[0].body)
|
||||
self.assertIn(self.approver_1, outbox[0].html)
|
||||
|
||||
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1, self.approver_2)
|
||||
self.assertEqual(len(outbox), 2)
|
||||
self.assertIn(self.approver_1, outbox[1].body)
|
||||
self.assertIn(self.approver_1, outbox[1].html)
|
||||
self.assertIn(self.approver_2, outbox[1].body)
|
||||
self.assertIn(self.approver_2, outbox[1].html)
|
||||
|
||||
db_emails = EmailModel.query.count()
|
||||
self.assertEqual(db_emails, 2)
|
||||
|
||||
def test_send_ramp_up_denied_email(self):
|
||||
with mail.record_messages() as outbox:
|
||||
send_ramp_up_denied_email(self.sender, self.recipients, self.approver_1)
|
||||
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Denied')
|
||||
self.assertIn(self.approver_1, outbox[0].body)
|
||||
self.assertIn(self.approver_1, outbox[0].html)
|
||||
|
||||
db_emails = EmailModel.query.count()
|
||||
self.assertEqual(db_emails, 1)
|
||||
|
||||
def test_send_send_ramp_up_denied_email_to_approver(self):
|
||||
with mail.record_messages() as outbox:
|
||||
send_ramp_up_denied_email_to_approver(
|
||||
self.sender, self.recipients, self.primary_investigator, self.approver_2
|
||||
)
|
||||
|
||||
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Denied')
|
||||
self.assertIn(self.primary_investigator, outbox[0].body)
|
||||
self.assertIn(self.primary_investigator, outbox[0].html)
|
||||
self.assertIn(self.approver_2, outbox[0].body)
|
||||
self.assertIn(self.approver_2, outbox[0].html)
|
||||
|
||||
db_emails = EmailModel.query.count()
|
||||
self.assertEqual(db_emails, 1)
|
|
@ -61,14 +61,14 @@ class TestFileService(BaseTest):
|
|||
|
||||
# Archive the file
|
||||
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
|
||||
self.assertEquals(1, len(file_models))
|
||||
self.assertEqual(1, len(file_models))
|
||||
file_model = file_models[0]
|
||||
file_model.archived = True
|
||||
db.session.add(file_model)
|
||||
|
||||
# Assure that the file no longer comes back.
|
||||
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
|
||||
self.assertEquals(0, len(file_models))
|
||||
self.assertEqual(0, len(file_models))
|
||||
|
||||
# Add the file again with different data
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
|
@ -91,7 +91,6 @@ class TestFilesApi(BaseTest):
|
|||
content_type='multipart/form-data', headers=self.logged_in_headers())
|
||||
self.assert_success(rv)
|
||||
|
||||
|
||||
def test_archive_file_no_longer_shows_up(self):
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
|
@ -109,21 +108,16 @@ class TestFilesApi(BaseTest):
|
|||
self.assert_success(rv)
|
||||
rv = self.app.get('/v1.0/file?workflow_id=%s' % workflow.id, headers=self.logged_in_headers())
|
||||
self.assert_success(rv)
|
||||
self.assertEquals(1, len(json.loads(rv.get_data(as_text=True))))
|
||||
self.assertEqual(1, len(json.loads(rv.get_data(as_text=True))))
|
||||
|
||||
file_model = db.session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all()
|
||||
self.assertEquals(1, len(file_model))
|
||||
self.assertEqual(1, len(file_model))
|
||||
file_model[0].archived = True
|
||||
db.session.commit()
|
||||
|
||||
rv = self.app.get('/v1.0/file?workflow_id=%s' % workflow.id, headers=self.logged_in_headers())
|
||||
self.assert_success(rv)
|
||||
self.assertEquals(0, len(json.loads(rv.get_data(as_text=True))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
self.assertEqual(0, len(json.loads(rv.get_data(as_text=True))))
|
||||
|
||||
def test_set_reference_file(self):
|
||||
file_name = "irb_document_types.xls"
|
||||
|
@ -285,8 +279,8 @@ class TestFilesApi(BaseTest):
|
|||
.filter(ApprovalModel.status == ApprovalStatus.PENDING.value)\
|
||||
.filter(ApprovalModel.study_id == workflow.study_id).all()
|
||||
|
||||
self.assertEquals(1, len(approvals))
|
||||
self.assertEquals(1, len(approvals[0].approval_files))
|
||||
self.assertEqual(1, len(approvals))
|
||||
self.assertEqual(1, len(approvals[0].approval_files))
|
||||
|
||||
|
||||
def test_change_primary_bpmn(self):
|
|
@ -1,55 +0,0 @@
|
|||
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc.services.mails import (
|
||||
send_ramp_up_submission_email,
|
||||
send_ramp_up_approval_request_email,
|
||||
send_ramp_up_approval_request_first_review_email,
|
||||
send_ramp_up_approved_email,
|
||||
send_ramp_up_denied_email,
|
||||
send_ramp_up_denied_email_to_approver
|
||||
)
|
||||
|
||||
|
||||
class TestMails(BaseTest):
|
||||
|
||||
def setUp(self):
|
||||
self.sender = 'sender@sartography.com'
|
||||
self.recipients = ['recipient@sartography.com']
|
||||
self.primary_investigator = 'Dr. Bartlett'
|
||||
self.approver_1 = 'Max Approver'
|
||||
self.approver_2 = 'Close Reviewer'
|
||||
|
||||
def test_send_ramp_up_submission_email(self):
|
||||
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1)
|
||||
self.assertTrue(True)
|
||||
|
||||
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1, self.approver_2)
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_send_ramp_up_approval_request_email(self):
|
||||
send_ramp_up_approval_request_email(self.sender, self.recipients, self.primary_investigator)
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_send_ramp_up_approval_request_first_review_email(self):
|
||||
send_ramp_up_approval_request_first_review_email(
|
||||
self.sender, self.recipients, self.primary_investigator
|
||||
)
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_send_ramp_up_approved_email(self):
|
||||
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1)
|
||||
self.assertTrue(True)
|
||||
|
||||
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1, self.approver_2)
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_send_ramp_up_denied_email(self):
|
||||
send_ramp_up_denied_email(self.sender, self.recipients, self.approver_1)
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_send_send_ramp_up_denied_email_to_approver(self):
|
||||
send_ramp_up_denied_email_to_approver(
|
||||
self.sender, self.recipients, self.primary_investigator, self.approver_2
|
||||
)
|
||||
self.assertTrue(True)
|
|
@ -47,7 +47,7 @@ class TestTasksApi(BaseTest):
|
|||
# The total number of tasks may change over time, as users move through gateways
|
||||
# branches may be pruned. As we hit parallel Multi-Instance new tasks may be created...
|
||||
self.assertIsNotNone(workflow.total_tasks)
|
||||
self.assertEquals(prev_completed_task_count + 1, workflow.completed_tasks)
|
||||
self.assertEqual(prev_completed_task_count + 1, workflow.completed_tasks)
|
||||
# Assure a record exists in the Task Events
|
||||
task_events = session.query(TaskEventModel) \
|
||||
.filter_by(workflow_id=workflow.id) \
|
||||
|
@ -56,25 +56,25 @@ class TestTasksApi(BaseTest):
|
|||
self.assertGreater(len(task_events), 0)
|
||||
event = task_events[0]
|
||||
self.assertIsNotNone(event.study_id)
|
||||
self.assertEquals("dhf8r", event.user_uid)
|
||||
self.assertEquals(workflow.id, event.workflow_id)
|
||||
self.assertEquals(workflow.workflow_spec_id, event.workflow_spec_id)
|
||||
self.assertEquals(workflow.spec_version, event.spec_version)
|
||||
self.assertEquals(WorkflowService.TASK_ACTION_COMPLETE, event.action)
|
||||
self.assertEquals(task_in.id, task_id)
|
||||
self.assertEquals(task_in.name, event.task_name)
|
||||
self.assertEquals(task_in.title, event.task_title)
|
||||
self.assertEquals(task_in.type, event.task_type)
|
||||
self.assertEquals("COMPLETED", event.task_state)
|
||||
self.assertEqual("dhf8r", event.user_uid)
|
||||
self.assertEqual(workflow.id, event.workflow_id)
|
||||
self.assertEqual(workflow.workflow_spec_id, event.workflow_spec_id)
|
||||
self.assertEqual(workflow.spec_version, event.spec_version)
|
||||
self.assertEqual(WorkflowService.TASK_ACTION_COMPLETE, event.action)
|
||||
self.assertEqual(task_in.id, task_id)
|
||||
self.assertEqual(task_in.name, event.task_name)
|
||||
self.assertEqual(task_in.title, event.task_title)
|
||||
self.assertEqual(task_in.type, event.task_type)
|
||||
self.assertEqual("COMPLETED", event.task_state)
|
||||
# Not sure what vodoo is happening inside of marshmallow to get me in this state.
|
||||
if isinstance(task_in.multi_instance_type, MultiInstanceType):
|
||||
self.assertEquals(task_in.multi_instance_type.value, event.mi_type)
|
||||
self.assertEqual(task_in.multi_instance_type.value, event.mi_type)
|
||||
else:
|
||||
self.assertEquals(task_in.multi_instance_type, event.mi_type)
|
||||
self.assertEqual(task_in.multi_instance_type, event.mi_type)
|
||||
|
||||
self.assertEquals(task_in.multi_instance_count, event.mi_count)
|
||||
self.assertEquals(task_in.multi_instance_index, event.mi_index)
|
||||
self.assertEquals(task_in.process_name, event.process_name)
|
||||
self.assertEqual(task_in.multi_instance_count, event.mi_count)
|
||||
self.assertEqual(task_in.multi_instance_index, event.mi_index)
|
||||
self.assertEqual(task_in.process_name, event.process_name)
|
||||
self.assertIsNotNone(event.date)
|
||||
|
||||
# Assure that there is data in the form_data
|
||||
|
|
Loading…
Reference in New Issue