Merge branch 'dev' into 152-workflow-spec-pull

This commit is contained in:
Dan 2021-01-21 12:30:43 -05:00
commit 5921ce9f10
45 changed files with 1868 additions and 785 deletions

398
Pipfile.lock generated
View File

@ -33,10 +33,10 @@
},
"aniso8601": {
"hashes": [
"sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072",
"sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a"
"sha256:246bf8d3611527030889e6df970878969d3a2f760ba3eb694fa1fb10e6ce53f9",
"sha256:51047d4fb51d7b8afd522b70f2d21a1b2487cbb7f7bd84ea852e9aa7808e7704"
],
"version": "==8.0.0"
"version": "==8.1.0"
},
"attrs": {
"hashes": [
@ -80,10 +80,10 @@
},
"certifi": {
"hashes": [
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.11.8"
"version": "==2020.12.5"
},
"cffi": {
"hashes": [
@ -108,12 +108,14 @@
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
@ -126,10 +128,10 @@
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==3.0.4"
"version": "==4.0.0"
},
"click": {
"hashes": [
@ -165,43 +167,58 @@
},
"coverage": {
"hashes": [
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
"sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
"sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
"sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
"sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
"sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
"sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
"sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
"sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
"sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
"sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
"sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
"sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
"sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
"sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
"sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
"sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
"sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
"sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
"sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
"sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
"sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
"sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
"sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
"sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
"sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
"sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
"sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
"sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
"sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
"sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
"sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
"sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
"sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
"sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
"sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4",
"sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879",
"sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f",
"sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4",
"sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044",
"sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e",
"sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899",
"sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f",
"sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448",
"sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714",
"sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2",
"sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d",
"sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd",
"sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7",
"sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"
],
"index": "pypi",
"version": "==5.3"
"version": "==5.3.1"
},
"deprecated": {
"hashes": [
@ -469,11 +486,11 @@
},
"marshmallow": {
"hashes": [
"sha256:73facc37462dfc0b27f571bdaffbef7709e19f7a616beb3802ea425b07843f4e",
"sha256:e26763201474b588d144dae9a32bdd945cd26a06c943bc746a6882e850475378"
"sha256:4ab2fdb7f36eb61c3665da67a7ce281c8900db08d72ba6bf0e695828253581f7",
"sha256:eca81d53aa4aafbc0e20566973d0d2e50ce8bf0ee15165bb799bec0df1e50177"
],
"index": "pypi",
"version": "==3.9.1"
"version": "==3.10.0"
},
"marshmallow-enum": {
"hashes": [
@ -548,40 +565,33 @@
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
],
"version": "==20.4"
"version": "==20.8"
},
"pandas": {
"hashes": [
"sha256:09e0503758ad61afe81c9069505f8cb8c1e36ea8cc1e6826a95823ef5b327daf",
"sha256:0a11a6290ef3667575cbd4785a1b62d658c25a2fd70a5adedba32e156a8f1773",
"sha256:0d9a38a59242a2f6298fff45d09768b78b6eb0c52af5919ea9e45965d7ba56d9",
"sha256:112c5ba0f9ea0f60b2cc38c25f87ca1d5ca10f71efbee8e0f1bee9cf584ed5d5",
"sha256:185cf8c8f38b169dbf7001e1a88c511f653fbb9dfa3e048f5e19c38049e991dc",
"sha256:3aa8e10768c730cc1b610aca688f588831fa70b65a26cb549fbb9f35049a05e0",
"sha256:41746d520f2b50409dffdba29a15c42caa7babae15616bcf80800d8cfcae3d3e",
"sha256:43cea38cbcadb900829858884f49745eb1f42f92609d368cabcc674b03e90efc",
"sha256:5378f58172bd63d8c16dd5d008d7dcdd55bf803fcdbe7da2dcb65dbbf322f05b",
"sha256:54404abb1cd3f89d01f1fb5350607815326790efb4789be60508f458cdd5ccbf",
"sha256:5dac3aeaac5feb1016e94bde851eb2012d1733a222b8afa788202b836c97dad5",
"sha256:5fdb2a61e477ce58d3f1fdf2470ee142d9f0dde4969032edaf0b8f1a9dafeaa2",
"sha256:6613c7815ee0b20222178ad32ec144061cb07e6a746970c9160af1ebe3ad43b4",
"sha256:6d2b5b58e7df46b2c010ec78d7fb9ab20abf1d306d0614d3432e7478993fbdb0",
"sha256:8a5d7e57b9df2c0a9a202840b2881bb1f7a648eba12dd2d919ac07a33a36a97f",
"sha256:8b4c2055ebd6e497e5ecc06efa5b8aa76f59d15233356eb10dad22a03b757805",
"sha256:a15653480e5b92ee376f8458197a58cca89a6e95d12cccb4c2d933df5cecc63f",
"sha256:a7d2547b601ecc9a53fd41561de49a43d2231728ad65c7713d6b616cd02ddbed",
"sha256:a979d0404b135c63954dea79e6246c45dd45371a88631cdbb4877d844e6de3b6",
"sha256:b1f8111635700de7ac350b639e7e452b06fc541a328cf6193cf8fc638804bab8",
"sha256:c5a3597880a7a29a31ebd39b73b2c824316ae63a05c3c8a5ce2aea3fc68afe35",
"sha256:c681e8fcc47a767bf868341d8f0d76923733cbdcabd6ec3a3560695c69f14a1e",
"sha256:cf135a08f306ebbcfea6da8bf775217613917be23e5074c69215b91e180caab4",
"sha256:e2b8557fe6d0a18db4d61c028c6af61bfed44ef90e419ed6fadbdc079eba141e"
"sha256:0be6102dd99910513e75ed6536284743ead810349c51bdeadd2a5b6649f30abb",
"sha256:272675a98fa4954b9fc0933df775596fc942e50015d7e75d8f19548808a2bfdf",
"sha256:2d8b4f532db37418121831a461fd107d826c240b098f52e7a1b4ab3d5aaa4fb2",
"sha256:33318fa24b192b1a4684347ff76679a7267fd4e547da9f71556a5914f0dc10e7",
"sha256:3bc6d2be03cb75981d8cbeda09503cd9d6d699fc0dc28a65e197165ad527b7b8",
"sha256:43482789c55cbabeed9482263cfc98a11e8fcae900cb63ef038948acb4a72570",
"sha256:616478c1bd8fe1e600f521ae2da434e021c11e7a4e5da3451d02906143d3629a",
"sha256:6c1a57e4d0d6f9633a07817c44e6b36d81c265fe4c52d0c0505513a2d0f7953c",
"sha256:7904ee438549b5223ce8dc008772458dd7c5cf0ccc64cf903e81202400702235",
"sha256:7b54c14130a3448d81eed1348f52429c23e27188d9db6e6d4afeae792bc49c11",
"sha256:8f92b07cdbfa3704d85b4264e52c216cafe6c0059b0d07cdad8cb29e0b90f2b8",
"sha256:91fd0b94e7b98528177a05e6f65efea79d7ef9dec15ee48c7c69fc39fdd87235",
"sha256:9c6692cea6d56da8650847172bdb148622f545e7782d17995822434c79d7a211",
"sha256:9e18631d996fe131de6cb31a8bdae18965cc8f39eb23fdfbbf42808ecc63dabf",
"sha256:cba93d4fd3b0a42858b2b599495aff793fb5d94587979f45a14177d1217ba446",
"sha256:e03386615b970b8b41da6a68afe717626741bb2431cec993640685614c0680e4",
"sha256:f8b87d2f541cd9bc4ecfe85a561abac85c33fe4de4ce70cca36b2768af2611f5"
],
"index": "pypi",
"version": "==1.1.4"
"version": "==1.2.0"
},
"psycopg2-binary": {
"hashes": [
@ -640,18 +650,18 @@
},
"pygithub": {
"hashes": [
"sha256:776befaddab9d8fddd525d52a6ca1ac228cf62b5b1e271836d766f4925e1452e",
"sha256:8ad656bf79958e775ec59f7f5a3dbcbadac12147ae3dc42708b951064096af15"
"sha256:300bc16e62886ca6537b0830e8f516ea4bc3ef12d308e0c5aff8bdbd099173d4",
"sha256:87afd6a67ea582aa7533afdbf41635725f13d12581faed7e3e04b1579c0c0627"
],
"index": "pypi",
"version": "==1.53"
"version": "==1.54.1"
},
"pygments": {
"hashes": [
"sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0",
"sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"
"sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716",
"sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"
],
"version": "==2.7.2"
"version": "==2.7.3"
},
"pyjwt": {
"hashes": [
@ -713,10 +723,10 @@
},
"pytz": {
"hashes": [
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
"sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
"sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
],
"version": "==2020.4"
"version": "==2020.5"
},
"pyyaml": {
"hashes": [
@ -738,19 +748,19 @@
},
"recommonmark": {
"hashes": [
"sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb",
"sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852"
"sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f",
"sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"
],
"index": "pypi",
"version": "==0.6.0"
"version": "==0.7.1"
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"index": "pypi",
"version": "==2.25.0"
"version": "==2.25.1"
},
"sentry-sdk": {
"extras": [
@ -779,19 +789,19 @@
},
"soupsieve": {
"hashes": [
"sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55",
"sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"
"sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851",
"sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e"
],
"markers": "python_version >= '3.0'",
"version": "==2.0.1"
"version": "==2.1"
},
"sphinx": {
"hashes": [
"sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300",
"sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"
"sha256:aeef652b14629431c82d3fe994ce39ead65b3fe87cf41b9a3714168ff8b83376",
"sha256:e450cb205ff8924611085183bf1353da26802ae73d9251a8fcdf220a8f8712ef"
],
"index": "pypi",
"version": "==3.3.1"
"version": "==3.4.1"
},
"sphinxcontrib-applehelp": {
"hashes": [
@ -837,50 +847,50 @@
},
"spiffworkflow": {
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "6b2ed24bb340ebd31049312bd321f66ebf7b6b26"
"ref": "6632930a28a790f3e804a99c7589593cd732a690"
},
"sqlalchemy": {
"hashes": [
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
"sha256:0157c269701d88f5faf1fa0e4560e4d814f210c01a5b55df3cab95e9346a8bcc",
"sha256:0a92745bb1ebbcb3985ed7bda379b94627f0edbc6c82e9e4bac4fb5647ae609a",
"sha256:0cca1844ba870e81c03633a99aa3dc62256fb96323431a5dec7d4e503c26372d",
"sha256:166917a729b9226decff29416f212c516227c2eb8a9c9f920d69ced24e30109f",
"sha256:1f5f369202912be72fdf9a8f25067a5ece31a2b38507bb869306f173336348da",
"sha256:2909dffe5c9a615b7e6c92d1ac2d31e3026dc436440a4f750f4749d114d88ceb",
"sha256:2b5dafed97f778e9901b79cc01b88d39c605e0545b4541f2551a2fd785adc15b",
"sha256:2e9bd5b23bba8ae8ce4219c9333974ff5e103c857d9ff0e4b73dc4cb244c7d86",
"sha256:3aa6d45e149a16aa1f0c46816397e12313d5e37f22205c26e06975e150ffcf2a",
"sha256:4bdbdb8ca577c6c366d15791747c1de6ab14529115a2eb52774240c412a7b403",
"sha256:53fd857c6c8ffc0aa6a5a3a2619f6a74247e42ec9e46b836a8ffa4abe7aab327",
"sha256:5cdfe54c1e37279dc70d92815464b77cd8ee30725adc9350f06074f91dbfeed2",
"sha256:5d92c18458a4aa27497a986038d5d797b5279268a2de303cd00910658e8d149c",
"sha256:632b32183c0cb0053194a4085c304bc2320e5299f77e3024556fa2aa395c2a8b",
"sha256:7c735c7a6db8ee9554a3935e741cf288f7dcbe8706320251eb38c412e6a4281d",
"sha256:7cd40cb4bc50d9e87b3540b23df6e6b24821ba7e1f305c1492b0806c33dbdbec",
"sha256:84f0ac4a09971536b38cc5d515d6add7926a7e13baa25135a1dbb6afa351a376",
"sha256:8dcbf377529a9af167cbfc5b8acec0fadd7c2357fc282a1494c222d3abfc9629",
"sha256:950f0e17ffba7a7ceb0dd056567bc5ade22a11a75920b0e8298865dc28c0eff6",
"sha256:9e379674728f43a0cd95c423ac0e95262500f9bfd81d33b999daa8ea1756d162",
"sha256:b15002b9788ffe84e42baffc334739d3b68008a973d65fad0a410ca5d0531980",
"sha256:b6f036ecc017ec2e2cc2a40615b41850dc7aaaea6a932628c0afc73ab98ba3fb",
"sha256:bad73f9888d30f9e1d57ac8829f8a12091bdee4949b91db279569774a866a18e",
"sha256:bbc58fca72ce45a64bb02b87f73df58e29848b693869e58bd890b2ddbb42d83b",
"sha256:bca4d367a725694dae3dfdc86cf1d1622b9f414e70bd19651f5ac4fb3aa96d61",
"sha256:be41d5de7a8e241864189b7530ca4aaf56a5204332caa70555c2d96379e18079",
"sha256:bf53d8dddfc3e53a5bda65f7f4aa40fae306843641e3e8e701c18a5609471edf",
"sha256:c092fe282de83d48e64d306b4bce03114859cdbfe19bf8a978a78a0d44ddadb1",
"sha256:c3ab23ee9674336654bf9cac30eb75ac6acb9150dc4b1391bec533a7a4126471",
"sha256:ce64a44c867d128ab8e675f587aae7f61bd2db836a3c4ba522d884cd7c298a77",
"sha256:d05cef4a164b44ffda58200efcb22355350979e000828479971ebca49b82ddb1",
"sha256:d2f25c7f410338d31666d7ddedfa67570900e248b940d186b48461bd4e5569a1",
"sha256:d3b709d64b5cf064972b3763b47139e4a0dc4ae28a36437757f7663f67b99710",
"sha256:e32e3455db14602b6117f0f422f46bc297a3853ae2c322ecd1e2c4c04daf6ed5",
"sha256:ed53209b5f0f383acb49a927179fa51a6e2259878e164273ebc6815f3a752465",
"sha256:f605f348f4e6a2ba00acb3399c71d213b92f27f2383fc4abebf7a37368c12142",
"sha256:fcdb3755a7c355bc29df1b5e6fb8226d5c8b90551d202d69d0076a8a5649d68b"
"sha256:04f995fcbf54e46cddeb4f75ce9dfc17075d6ae04ac23b2bacb44b3bc6f6bf11",
"sha256:0c6406a78a714a540d980a680b86654feadb81c8d0eecb59f3d6c554a4c69f19",
"sha256:0c72b90988be749e04eff0342dcc98c18a14461eb4b2ad59d611b57b31120f90",
"sha256:108580808803c7732f34798eb4a329d45b04c562ed83ee90f09f6a184a42b766",
"sha256:1418f5e71d6081aa1095a1d6b567a562d2761996710bdce9b6e6ba20a03d0864",
"sha256:17610d573e698bf395afbbff946544fbce7c5f4ee77b5bcb1f821b36345fae7a",
"sha256:216ba5b4299c95ed179b58f298bda885a476b16288ab7243e89f29f6aeced7e0",
"sha256:2ff132a379838b1abf83c065be54cef32b47c987aedd06b82fc76476c85225eb",
"sha256:314f5042c0b047438e19401d5f29757a511cfc2f0c40d28047ca0e4c95eabb5b",
"sha256:318b5b727e00662e5fc4b4cd2bf58a5116d7c1b4dd56ffaa7d68f43458a8d1ed",
"sha256:3ab5b44a07b8c562c6dcb7433c6a6c6e03266d19d64f87b3333eda34e3b9936b",
"sha256:426ece890153ccc52cc5151a1a0ed540a5a7825414139bb4c95a868d8da54a52",
"sha256:491fe48adc07d13e020a8b07ef82eefc227003a046809c121bea81d3dbf1832d",
"sha256:4a84c7c7658dd22a33dab2e2aa2d17c18cb004a42388246f2e87cb4085ef2811",
"sha256:54da615e5b92c339e339fe8536cce99fe823b6ed505d4ea344852aefa1c205fb",
"sha256:5a7f224cdb7233182cec2a45d4c633951268d6a9bcedac37abbf79dd07012aea",
"sha256:61628715931f4962e0cdb2a7c87ff39eea320d2aa96bd471a3c293d146f90394",
"sha256:62285607a5264d1f91590abd874d6a498e229d5840669bd7d9f654cfaa599bd0",
"sha256:62fb881ba51dbacba9af9b779211cf9acff3442d4f2993142015b22b3cd1f92a",
"sha256:68428818cf80c60dc04aa0f38da20ad39b28aba4d4d199f949e7d6e04444ea86",
"sha256:6aaa13ee40c4552d5f3a59f543f0db6e31712cc4009ec7385407be4627259d41",
"sha256:70121f0ae48b25ef3e56e477b88cd0b0af0e1f3a53b5554071aa6a93ef378a03",
"sha256:715b34578cc740b743361f7c3e5f584b04b0f1344f45afc4e87fbac4802eb0a0",
"sha256:758fc8c4d6c0336e617f9f6919f9daea3ab6bb9b07005eda9a1a682e24a6cacc",
"sha256:7d4b8de6bb0bc736161cb0bbd95366b11b3eb24dd6b814a143d8375e75af9990",
"sha256:81d8d099a49f83111cce55ec03cc87eef45eec0d90f9842b4fc674f860b857b0",
"sha256:888d5b4b5aeed0d3449de93ea80173653e939e916cc95fe8527079e50235c1d2",
"sha256:95bde07d19c146d608bccb9b16e144ec8f139bcfe7fd72331858698a71c9b4f5",
"sha256:9bf572e4f5aa23f88dd902f10bb103cb5979022a38eec684bfa6d61851173fec",
"sha256:bab5a1e15b9466a25c96cda19139f3beb3e669794373b9ce28c4cf158c6e841d",
"sha256:bd4b1af45fd322dcd1fb2a9195b4f93f570d1a5902a842e3e6051385fac88f9c",
"sha256:bde677047305fe76c7ee3e4492b545e0018918e44141cc154fe39e124e433991",
"sha256:c389d7cc2b821853fb018c85457da3e7941db64f4387720a329bc7ff06a27963",
"sha256:d055ff750fcab69ca4e57b656d9c6ad33682e9b8d564f2fbe667ab95c63591b0",
"sha256:d53f59744b01f1440a1b0973ed2c3a7de204135c593299ee997828aad5191693",
"sha256:f115150cc4361dd46153302a640c7fa1804ac207f9cc356228248e351a8b4676",
"sha256:f1e88b30da8163215eab643962ae9d9252e47b4ea53404f2c4f10f24e70ddc62",
"sha256:f8191fef303025879e6c3548ecd8a95aafc0728c764ab72ec51a0bdf0c91a341"
],
"version": "==1.3.20"
"version": "==1.3.22"
},
"swagger-ui-bundle": {
"hashes": [
@ -942,11 +952,11 @@
},
"xlrd": {
"hashes": [
"sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2",
"sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde"
"sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd",
"sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"
],
"index": "pypi",
"version": "==1.2.0"
"version": "==2.0.1"
},
"xlsxwriter": {
"hashes": [
@ -967,43 +977,58 @@
},
"coverage": {
"hashes": [
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
"sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
"sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
"sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
"sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
"sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
"sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
"sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
"sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
"sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
"sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
"sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
"sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
"sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
"sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
"sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
"sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
"sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
"sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
"sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
"sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
"sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
"sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
"sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
"sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
"sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
"sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
"sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
"sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
"sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
"sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
"sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
"sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
"sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
"sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
"sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4",
"sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879",
"sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f",
"sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4",
"sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044",
"sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e",
"sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899",
"sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f",
"sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448",
"sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714",
"sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2",
"sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d",
"sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd",
"sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7",
"sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"
],
"index": "pypi",
"version": "==5.3"
"version": "==5.3.1"
},
"iniconfig": {
"hashes": [
@ -1014,10 +1039,10 @@
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
],
"version": "==20.4"
"version": "==20.8"
},
"pbr": {
"hashes": [
@ -1036,10 +1061,10 @@
},
"py": {
"hashes": [
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"version": "==1.9.0"
"version": "==1.10.0"
},
"pyparsing": {
"hashes": [
@ -1050,18 +1075,11 @@
},
"pytest": {
"hashes": [
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
"sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
"sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
],
"index": "pypi",
"version": "==6.1.2"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
"version": "==6.2.1"
},
"toml": {
"hashes": [

View File

@ -64,6 +64,44 @@ it should always be visible in the Docker Desktop Daskboard with a friendly litt
stop button for your clicking pleasure.
### Environment Setup Part #2
If you want to run CR-Connect in development mode you must have the following two services running:
1. `Postgres Docker`: There is a sub-directory called Postgres that contains a docker image that will set up an empty
database for CR-Connect, and for Protocol Builder Mock, mentioned below. For must systems, you can 'cd' into this
directory and just run start.sh to fire up the Postgres service, and stop.sh to shut it back down again.
create .env file in /postgres with the following two lines in it:
```
DB_USER=crc_user
DB_PASS=crc_pass
```
With this in place, from the command line:
```bash
cd postgres
./start.sh
```
You can now build the database structure in the newly created database with the following lines
```baseh
cd .. (into base directory)
flask db upgrade
flask load-example-data (this creates some basic workflows for you to use)
```
2. `Protocol Builder Mock`: We created a mock of the Protocol Builder, a critical service at UVA that is a deep
dependency for CR-Connect. You can find the details here: [Protocol Builder Mock](https://github.com/sartography/protocol-builder-mock)
Be sure this is up and running on Port 5002 or you will encounter errors when the system starts up.
With Protocol Builder Mock up and running, visit http://localhost:5001 and create a study. Set the user
and primary investigator to dhf8r - which is a user in the mock ldap service, and this will later show up when you
fire up the interface.
### Configuration
1. `instance/config.py`: This will
### Project Initialization
1. Clone this repository.
2. In PyCharm:

View File

@ -89,6 +89,7 @@ def load_example_data():
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
ExampleDataLoader().load_all()
ExampleDataLoader().load_default_user()
@app.cli.command()
@ -98,6 +99,13 @@ def load_example_rrt_data():
ExampleDataLoader.clean_db()
ExampleDataLoader().load_rrt()
@app.cli.command()
def load_reference_files():
"""Load example data into the database."""
from example_data import ExampleDataLoader
ExampleDataLoader().load_reference_documents()
@app.cli.command()
def clear_db():
"""Load example data into the database."""

View File

@ -850,20 +850,8 @@ paths:
format: int32
get:
operationId: crc.api.workflow.get_workflow
summary: Returns a workflow, can also be used to do a soft or hard reset on the workflow.
summary: Returns a workflow.
parameters:
- name: soft_reset
in: query
required: false
description: Set this to true to use the latest workflow specification to load minor modifications to the spec.
schema:
type: boolean
- name: hard_reset
in: query
required: false
description: Set this to true to reset the workflow
schema:
type: boolean
- name: do_engine_steps
in: query
required: false
@ -889,6 +877,35 @@ paths:
responses:
'204':
description: The workflow was removed
/workflow/{workflow_id}/restart:
parameters:
- name: workflow_id
in: path
required: true
description: The id of the workflow
schema:
type: integer
format: int32
get:
operationId: crc.api.workflow.restart_workflow
summary: Restarts a workflow with the latest spec. Can also clear data.
parameters:
- name: clear_data
in: query
required: false
description: Set this to true to clear data when starting workflow.
schema:
type: boolean
tags:
- Workflows and Tasks
responses:
'200':
description: Returns updated workflow, possibly without data.
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
/workflow/{workflow_id}/task/{task_id}/data:
parameters:
- name: workflow_id

View File

@ -4,6 +4,8 @@ from flask import g
from crc import ma, app
import sentry_sdk
class ApiError(Exception):
def __init__(self, code, message, status_code=400,
@ -16,6 +18,13 @@ class ApiError(Exception):
self.file_name = file_name or "" # OPTIONAL: The file that caused the error.
self.tag = tag or "" # OPTIONAL: The XML Tag that caused the issue.
self.task_data = task_data or "" # OPTIONAL: A snapshot of data connected to the task when error ocurred.
if hasattr(g,'user'):
user = g.user.uid
else:
user = 'Unknown'
self.task_user = user
# This is for sentry logging into Slack
sentry_sdk.set_context("User", {'user': user})
Exception.__init__(self, self.message)
@classmethod
@ -59,7 +68,7 @@ class ApiError(Exception):
class ApiErrorSchema(ma.Schema):
class Meta:
fields = ("code", "message", "workflow_name", "file_name", "task_name", "task_id",
"task_data")
"task_data", "task_user")
@app.errorhandler(ApiError)

View File

@ -1,11 +1,13 @@
import uuid
from flask import g
from crc import session, db, app
from crc import session
from crc.api.common import ApiError, ApiErrorSchema
from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema
from crc.models.file import FileModel, LookupDataSchema, FileDataModel
from crc.models.api_models import WorkflowApiSchema
from crc.models.file import FileModel, LookupDataSchema
from crc.models.study import StudyModel, WorkflowMetadata
from crc.models.task_event import TaskEventModel, TaskEventModelSchema, TaskEvent, TaskEventSchema
from crc.models.task_event import TaskEventModel, TaskEvent, TaskEventSchema
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \
WorkflowSpecCategoryModelSchema
from crc.services.file_service import FileService
@ -15,6 +17,7 @@ from crc.services.user_service import UserService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
def all_specifications():
schema = WorkflowSpecModelSchema(many=True)
return schema.dump(session.query(WorkflowSpecModel).all())
@ -93,21 +96,29 @@ def delete_workflow_specification(spec_id):
session.commit()
def get_workflow(workflow_id, soft_reset=False, hard_reset=False, do_engine_steps=True):
"""Soft reset will attempt to update to the latest spec without starting over,
Hard reset will update to the latest spec and start from the beginning.
Read Only will return the workflow in a read only state, without running any
engine tasks or logging any events. """
def get_workflow(workflow_id, do_engine_steps=True):
"""Retrieve workflow based on workflow_id, and return it in the last saved State.
If do_engine_steps is False, return the workflow without running any engine tasks or logging any events. """
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset)
processor = WorkflowProcessor(workflow_model)
if do_engine_steps:
processor.do_engine_steps()
processor.save()
WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
return WorkflowApiSchema().dump(workflow_api_model)
def restart_workflow(workflow_id, clear_data=False):
"""Restart a workflow with the latest spec.
Clear data allows user to restart the workflow without previous data."""
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
WorkflowProcessor.reset(workflow_model, clear_data=clear_data)
return get_workflow(workflow_model.id)
def get_task_events(action = None, workflow = None, study = None):
"""Provides a way to see a history of what has happened, or get a list of tasks that need your attention."""
query = session.query(TaskEventModel).filter(TaskEventModel.user_uid == g.user.uid)
@ -145,7 +156,7 @@ def set_current_task(workflow_id, task_id):
"currently set to COMPLETE or READY.")
# If we have an interrupt task, run it.
processor.bpmn_workflow.cancel_notify()
processor.cancel_notify()
# Only reset the token if the task doesn't already have it.
if spiff_task.state == spiff_task.COMPLETED:

View File

@ -1,7 +1,5 @@
import hashlib
import json
import pandas as pd
import requests
from crc import session, app
from crc.api.common import ApiError
from crc.models.file import FileModel, FileDataModel
@ -47,42 +45,55 @@ def get_changed_workflows(remote,as_df=False):
# get the local thumbprints & make sure that 'workflow_spec_id' is a column, not an index
local = get_all_spec_state_dataframe().reset_index()
if local.empty:
# return the list as a dict, let swagger convert it to json
remote_workflows['new'] = True
if as_df:
return remote_workflows
else:
return remote_workflows.reset_index().to_dict(orient='records')
# merge these on workflow spec id and hash - this will
# make two different date columns date_x and date_y
different = remote_workflows.merge(local,
different = remote_workflows.merge(local,
right_on=['workflow_spec_id','md5_hash'],
left_on=['workflow_spec_id','md5_hash'],
how = 'outer' ,
indicator=True).loc[lambda x : x['_merge']!='both']
if len(different)==0:
return []
# each line has a tag on it - if was in the left or the right,
# label it so we know if that was on the remote or local machine
different.loc[different['_merge']=='left_only','location'] = 'remote'
different.loc[different['_merge']=='right_only','location'] = 'local'
# this takes the different date_created_x and date-created_y columns and
# combines them back into one date_created column
index = different['date_created_x'].isnull()
different.loc[index,'date_created_x'] = different[index]['date_created_y']
different = different[['workflow_spec_id','date_created_x','location']].copy()
different.columns=['workflow_spec_id','date_created','location']
# If there are no differences, then we can just return.
if not different.empty:
# our different list will have multiple entries for a workflow if there is a version on either side
# we want to grab the most recent one, so we sort and grab the most recent one for each workflow
changedfiles = different.sort_values('date_created',ascending=False).groupby('workflow_spec_id').first()
# each line has a tag on it - if was in the left or the right,
# label it so we know if that was on the remote or local machine
different.loc[different['_merge']=='left_only','location'] = 'remote'
different.loc[different['_merge']=='right_only','location'] = 'local'
# get an exclusive or list of workflow ids - that is we want lists of files that are
# on one machine or the other, but not both
remote_spec_ids = remote_workflows[['workflow_spec_id']]
local_spec_ids = local[['workflow_spec_id']]
left = remote_spec_ids[~remote_spec_ids['workflow_spec_id'].isin(local_spec_ids['workflow_spec_id'])]
right = local_spec_ids[~local_spec_ids['workflow_spec_id'].isin(remote_spec_ids['workflow_spec_id'])]
# this takes the different date_created_x and date-created_y columns and
# combines them back into one date_created column
index = different['date_created_x'].isnull()
different.loc[index,'date_created_x'] = different[index]['date_created_y']
different = different[['workflow_spec_id','date_created_x','location']].copy()
different.columns=['workflow_spec_id','date_created','location']
# flag files as new that are only on the remote box and remove the files that are only on the local box
changedfiles['new'] = False
changedfiles.loc[changedfiles.index.isin(left['workflow_spec_id']), 'new'] = True
output = changedfiles[~changedfiles.index.isin(right['workflow_spec_id'])]
# our different list will have multiple entries for a workflow if there is a version on either side
# we want to grab the most recent one, so we sort and grab the most recent one for each workflow
changedfiles = different.sort_values('date_created',ascending=False).groupby('workflow_spec_id').first()
# get an exclusive or list of workflow ids - that is we want lists of files that are
# on one machine or the other, but not both
remote_spec_ids = remote_workflows[['workflow_spec_id']]
local_spec_ids = local[['workflow_spec_id']]
left = remote_spec_ids[~remote_spec_ids['workflow_spec_id'].isin(local_spec_ids['workflow_spec_id'])]
right = local_spec_ids[~local_spec_ids['workflow_spec_id'].isin(remote_spec_ids['workflow_spec_id'])]
# flag files as new that are only on the remote box and remove the files that are only on the local box
changedfiles['new'] = False
changedfiles.loc[changedfiles.index.isin(left['workflow_spec_id']), 'new'] = True
output = changedfiles[~changedfiles.index.isin(right['workflow_spec_id'])]
else:
output = different
# return the list as a dict, let swagger convert it to json
if as_df:
@ -366,6 +377,10 @@ def get_all_spec_state_dataframe():
else:
df = pd.DataFrame(filelist)
# If the file list is empty, return an empty data frame
if df.empty:
return df
# get a distinct list of file_model_id's with the most recent file_data retained
df = df.sort_values('date_created').drop_duplicates(['file_model_id'],keep='last').copy()

View File

@ -1,6 +1,7 @@
import enum
import marshmallow
from SpiffWorkflow.navigation import NavItem
from marshmallow import INCLUDE
from marshmallow_enum import EnumField
@ -15,22 +16,6 @@ class MultiInstanceType(enum.Enum):
sequential = "sequential"
class NavigationItem(object):
def __init__(self, id, task_id, name, title, backtracks, level, indent, child_count, state, is_decision,
task=None, lane=None):
self.id = id
self.task_id = task_id
self.name = name,
self.title = title
self.backtracks = backtracks
self.level = level
self.indent = indent
self.child_count = child_count
self.state = state
self.is_decision = is_decision
self.task = task
self.lane = lane
class Task(object):
##########################################################################
@ -43,6 +28,7 @@ class Task(object):
# Autocomplete field
FIELD_TYPE_AUTO_COMPLETE = "autocomplete"
FIELD_TYPE_AUTO_COMPLETE_MAX = "autocomplete_num" # Not used directly, passed in from the front end.
# Required field
FIELD_CONSTRAINT_REQUIRED = "required"
@ -158,15 +144,28 @@ class TaskSchema(ma.Schema):
class NavigationItemSchema(ma.Schema):
class Meta:
fields = ["id", "task_id", "name", "title", "backtracks", "level", "indent", "child_count", "state",
"is_decision", "task", "lane"]
fields = ["spec_id", "name", "spec_type", "task_id", "description", "backtracks", "indent",
"lane", "state", "children"]
unknown = INCLUDE
task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False, allow_none=True)
state = marshmallow.fields.String(required=False, allow_none=True)
description = marshmallow.fields.String(required=False, allow_none=True)
backtracks = marshmallow.fields.String(required=False, allow_none=True)
lane = marshmallow.fields.String(required=False, allow_none=True)
title = marshmallow.fields.String(required=False, allow_none=True)
task_id = marshmallow.fields.String(required=False, allow_none=True)
children = marshmallow.fields.List(marshmallow.fields.Nested(lambda: NavigationItemSchema()))
@marshmallow.post_load
def make_nav(self, data, **kwargs):
state = data.pop('state', None)
task_id = data.pop('task_id', None)
children = data.pop('children', [])
spec_type = data.pop('spec_type', None)
item = NavItem(**data)
item.state = state
item.task_id = task_id
item.children = children
item.spec_type = spec_type
return item
class WorkflowApi(object):
def __init__(self, id, status, next_task, navigation,

View File

@ -60,7 +60,6 @@ class StudyModel(db.Model):
self.last_updated = pbs.DATE_MODIFIED
self.irb_status = IrbStatus.incomplete_in_protocol_builder
self.status = StudyStatus.in_progress
class StudyEvent(db.Model):

View File

@ -1,3 +1,5 @@
import re
import markdown
from jinja2 import Template
@ -14,23 +16,26 @@ class Email(Script):
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.
Creates an email, using the provided arguments. The first argument is the subject of the email,
all subsequent arguments should be email addresses in quotes, or variables containing an email address or a list
of email addresses."
The "documentation" should contain markdown that will become the body of the email message.
Example:
Email Subject ApprvlApprvr1 PIComputingID
email ("My Subject", "dhf8r@virginia.edu", pi.email)
"""
def do_task_validate_only(self, task, *args, **kwargs):
self.get_subject(task, args)
self.get_users_info(task, args)
self.get_email_recipients(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)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
if len(args) < 1:
raise ApiError(code="missing_argument",
message="Email script requires a subject and at least one email address as arguments")
subject = args[0]
recipients = self.get_email_recipients(task, args)
content, content_html = self.get_content(task)
if recipients:
send_mail(
@ -41,44 +46,53 @@ Email Subject ApprvlApprvr1 PIComputingID
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.")
def check_valid_email(self, email):
# regex from https://emailregex.com/
regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
if (re.search(regex, email)):
return True
else:
return False
def get_email_recipients(self, task, args):
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}', exc_info=True)
continue
user_info = LdapService.user_info(uid)
email = user_info.email_address
emails.append(user_info.email_address)
if not isinstance(email, str):
if len(args) < 2:
raise ApiError(code="missing_argument",
message="Email script requires at least one email address as an argument. "
"Multiple email addresses are accepted.")
# Every argument following the subject should be an email, or a list of emails.
for arg in args[1:]:
if isinstance(arg, str):
emails_to_check = [arg]
elif isinstance(arg, list):
emails_to_check = arg
else:
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__)
message=f"Email script requires a valid email address, but received '{arg}'")
for e in emails_to_check:
if self.check_valid_email(e):
emails.append(e)
else:
raise ApiError(code="invalid_argument",
message="The email address you provided could not be parsed. "
"The value you provided is '%s" % e)
return emails
def get_subject(self, task, args):
if len(args) < 1:
# subject = ''
if len(args[0]) < 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.")
message="No subject was provided for the email message.")
subject = args[0]
if not isinstance(subject, str):
if not subject or 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__)
message="The subject you provided could not be parsed. "
"The value is \"%s\" " % subject)
return subject

View File

@ -0,0 +1,15 @@
from crc.scripts.script import Script
from crc.services.failing_service import FailingService
class FailingScript(Script):
def get_description(self):
return """It fails"""
def do_task_validate_only(self, task, *args, **kwargs):
pass
def do_task(self, task, *args, **kwargs):
FailingService.fail_as_service()

View File

@ -0,0 +1,23 @@
from crc.scripts.script import Script
from crc.services.file_service import FileService
class IsFileUploaded(Script):
def get_description(self):
return """Test whether a file is uploaded for a study.
Pass in the IRB Doc Code for the file."""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
doc_code = args[0]
files = FileService.get_files_for_study(study_id)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
files = FileService.get_files_for_study(study_id)
if len(files) > 0:
doc_code = args[0]
for file in files:
if doc_code == file.irb_doc_code:
return True
return False

View File

@ -326,7 +326,7 @@ Returns information specific to the protocol.
"IS_HGT": None,
"IS_IBC": None,
"IS_IDE": None,
"IS_IND": None,
"IS_IND": 1,
"IS_MENTAL_IMPAIRMENT_POP": None,
"IS_MINOR": None,
"IS_MINOR_PARTICIPANT": None,
@ -407,9 +407,11 @@ Returns information specific to the protocol.
retval = StudyService().get_documents_status(study_id)
if cmd == 'protocol':
retval = StudyService().get_protocol(study_id)
if isinstance(retval, list):
retval = [Box(item) for item in retval]
if isinstance(retval,dict) and prefix is not None:
return Box({x:retval[x] for x in retval.keys() if x[:len(prefix)] == prefix})
elif isinstance(retval,dict):
elif isinstance(retval,dict) :
return Box(retval)
else:
return retval

View File

@ -0,0 +1,11 @@
from crc.api.common import ApiError
class FailingService(object):
@staticmethod
def fail_as_service():
"""It fails"""
raise ApiError(code='bad_service',
message='This is my failing service')

View File

@ -6,8 +6,8 @@ from github import Github, GithubObject, UnknownObjectException
from uuid import UUID
from lxml import etree
import flask
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from lxml.etree import XMLSyntaxError
from pandas import ExcelFile
from sqlalchemy import desc
from sqlalchemy.exc import IntegrityError
@ -35,14 +35,22 @@ class FileService(object):
def add_workflow_spec_file(workflow_spec: WorkflowSpecModel,
name, content_type, binary_data, primary=False, is_status=False):
"""Create a new file and associate it with a workflow spec."""
file_model = FileModel(
workflow_spec_id=workflow_spec.id,
name=name,
primary=primary,
is_status=is_status,
)
# Raise ApiError if the file already exists
if session.query(FileModel)\
.filter(FileModel.workflow_spec_id == workflow_spec.id)\
.filter(FileModel.name == name).first():
return FileService.update_file(file_model, binary_data, content_type)
raise ApiError(code="Duplicate File",
message='If you want to replace the file, use the update mechanism.')
else:
file_model = FileModel(
workflow_spec_id=workflow_spec.id,
name=name,
primary=primary,
is_status=is_status,
)
return FileService.update_file(file_model, binary_data, content_type)
@staticmethod
def is_allowed_document(code):
@ -82,7 +90,7 @@ class FileService(object):
you get '1.0' rather than '1'
fixme: This is stupid stupid slow. Place it in the database and just check if it is up to date."""
data_model = FileService.get_reference_file_data(reference_file_name)
xls = ExcelFile(data_model.data)
xls = ExcelFile(data_model.data, engine='openpyxl')
df = xls.parse(xls.sheet_names[0])
for c in int_columns:
df[c] = df[c].fillna(0)
@ -153,8 +161,11 @@ class FileService(object):
# If this is a BPMN, extract the process id.
if file_model.type == FileType.bpmn:
bpmn: etree.Element = etree.fromstring(binary_data)
file_model.primary_process_id = FileService.get_process_id(bpmn)
try:
bpmn: etree.Element = etree.fromstring(binary_data)
file_model.primary_process_id = FileService.get_process_id(bpmn)
except XMLSyntaxError as xse:
raise ApiError("invalid_xml", "Failed to parse xml: " + str(xse), file_name=file_model.name)
new_file_data_model = FileDataModel(
data=binary_data, file_model_id=file_model.id, file_model=file_model,

View File

@ -21,6 +21,7 @@ import crc
from crc import session, app
from crc.api.common import ApiError
from crc.models.file import FileDataModel, FileModel, FileType
from crc.models.task_event import TaskEventModel
from crc.models.workflow import WorkflowStatus, WorkflowModel, WorkflowSpecDependencyFile
from crc.scripts.script import Script
from crc.services.file_service import FileService
@ -32,6 +33,14 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
It will execute python code read in from the bpmn. It will also make any scripts in the
scripts directory available for execution. """
def evaluate(self, task, expression):
"""
Evaluate the given expression, within the context of the given task and
return the result.
"""
return self.evaluate_expression(task, expression)
def execute(self, task: SpiffTask, script, data):
study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
@ -100,8 +109,25 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
Evaluate the given expression, within the context of the given task and
return the result.
"""
exp, valid = self.validateExpression(expression)
return self._eval(exp, **task.data)
study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
if WorkflowProcessor.WORKFLOW_ID_KEY in task.workflow.data:
workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
else:
workflow_id = None
try:
if task.workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY]:
augmentMethods = Script.generate_augmented_validate_list(task, study_id, workflow_id)
else:
augmentMethods = Script.generate_augmented_list(task, study_id, workflow_id)
exp, valid = self.validateExpression(expression)
return self._eval(exp, externalMethods=augmentMethods, **task.data)
except Exception as e:
raise WorkflowTaskExecException(task,
"Error evaluating expression "
"'%s', %s" % (expression, str(e)))
@staticmethod
def camel_to_snake(camel):
@ -125,19 +151,12 @@ class WorkflowProcessor(object):
STUDY_ID_KEY = "study_id"
VALIDATION_PROCESS_KEY = "validate_only"
def __init__(self, workflow_model: WorkflowModel,
soft_reset=False, hard_reset=False, validate_only=False):
"""Create a Workflow Processor based on the serialized information available in the workflow model.
If soft_reset is set to true, it will try to use the latest version of the workflow specification
without resetting to the beginning of the workflow. This will work for some minor changes to the spec.
If hard_reset is set to true, it will use the latest spec, and start the workflow over from the beginning.
which should work in casees where a soft reset fails.
If neither flag is set, it will use the same version of the specification that was used to originally
create the workflow model. """
def __init__(self, workflow_model: WorkflowModel, validate_only=False):
"""Create a Workflow Processor based on the serialized information available in the workflow model."""
self.workflow_model = workflow_model
if soft_reset or len(workflow_model.dependencies) == 0: # Depenencies of 0 means the workflow was never started.
if workflow_model.bpmn_workflow_json is None: # The workflow was never started.
self.spec_data_files = FileService.get_spec_data_files(
workflow_spec_id=workflow_model.workflow_spec_id)
spec = self.get_spec(self.spec_data_files, workflow_model.workflow_spec_id)
@ -162,22 +181,14 @@ class WorkflowProcessor(object):
# can then load data as needed.
self.bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = workflow_model.id
workflow_model.bpmn_workflow_json = WorkflowProcessor._serializer.serialize_workflow(
self.bpmn_workflow,include_spec=True)
self.bpmn_workflow, include_spec=True)
self.save()
except MissingSpecError as ke:
raise ApiError(code="unexpected_workflow_structure",
message="Failed to deserialize workflow"
" '%s' version %s, due to a mis-placed or missing task '%s'" %
(self.workflow_spec_id, self.get_version_string(), str(ke)) +
" This is very likely due to a soft reset where there was a structural change.")
if hard_reset:
# Now that the spec is loaded, get the data and rebuild the bpmn with the new details
self.hard_reset()
workflow_model.bpmn_workflow_json = WorkflowProcessor._serializer.serialize_workflow(self.bpmn_workflow)
self.save()
if soft_reset:
self.save()
(self.workflow_spec_id, self.get_version_string(), str(ke)))
# set whether this is the latest spec file.
if self.spec_data_files == FileService.get_spec_data_files(workflow_spec_id=workflow_model.workflow_spec_id):
@ -185,6 +196,21 @@ class WorkflowProcessor(object):
else:
self.is_latest_spec = False
@classmethod
def reset(cls, workflow_model, clear_data=False):
print('WorkflowProcessor: reset: ')
workflow_model.bpmn_workflow_json = None
if clear_data:
# Clear form_data from task_events
task_events = session.query(TaskEventModel). \
filter(TaskEventModel.workflow_id == workflow_model.id).all()
for task_event in task_events:
task_event.form_data = {}
session.add(task_event)
session.commit()
return cls(workflow_model)
def __get_bpmn_workflow(self, workflow_model: WorkflowModel, spec: WorkflowSpec, validate_only=False):
if workflow_model.bpmn_workflow_json:
bpmn_workflow = self._serializer.deserialize_workflow(workflow_model.bpmn_workflow_json,
@ -330,18 +356,18 @@ class WorkflowProcessor(object):
else:
return WorkflowStatus.waiting
def hard_reset(self):
"""Recreate this workflow. This will be useful when a workflow specification changes.
"""
self.spec_data_files = FileService.get_spec_data_files(workflow_spec_id=self.workflow_spec_id)
new_spec = WorkflowProcessor.get_spec(self.spec_data_files, self.workflow_spec_id)
new_bpmn_workflow = BpmnWorkflow(new_spec, script_engine=self._script_engine)
new_bpmn_workflow.data = self.bpmn_workflow.data
try:
new_bpmn_workflow.do_engine_steps()
except WorkflowException as we:
raise ApiError.from_task_spec("hard_reset_engine_steps_error", str(we), we.sender)
self.bpmn_workflow = new_bpmn_workflow
# def hard_reset(self):
# """Recreate this workflow. This will be useful when a workflow specification changes.
# """
# self.spec_data_files = FileService.get_spec_data_files(workflow_spec_id=self.workflow_spec_id)
# new_spec = WorkflowProcessor.get_spec(self.spec_data_files, self.workflow_spec_id)
# new_bpmn_workflow = BpmnWorkflow(new_spec, script_engine=self._script_engine)
# new_bpmn_workflow.data = self.bpmn_workflow.data
# try:
# new_bpmn_workflow.do_engine_steps()
# except WorkflowException as we:
# raise ApiError.from_task_spec("hard_reset_engine_steps_error", str(we), we.sender)
# self.bpmn_workflow = new_bpmn_workflow
def get_status(self):
return self.status_of(self.bpmn_workflow)
@ -352,6 +378,12 @@ class WorkflowProcessor(object):
except WorkflowTaskExecException as we:
raise ApiError.from_task("task_error", str(we), we.task)
def cancel_notify(self):
try:
self.bpmn_workflow.cancel_notify()
except WorkflowTaskExecException as we:
raise ApiError.from_task("task_error", str(we), we.task)
def serialize(self):
return self._serializer.serialize_workflow(self.bpmn_workflow,include_spec=True)

View File

@ -1,13 +1,12 @@
import copy
import json
import string
import uuid
from datetime import datetime
import random
import string
from datetime import datetime
from typing import List
import jinja2
from SpiffWorkflow import Task as SpiffTask, WorkflowException
from SpiffWorkflow import Task as SpiffTask, WorkflowException, NavItem
from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.MultiInstanceTask import MultiInstanceTask
@ -15,17 +14,16 @@ from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask
from SpiffWorkflow.bpmn.specs.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.specs import CancelTask, StartTask
from SpiffWorkflow.specs import CancelTask, StartTask, MultiChoice
from SpiffWorkflow.util.deep_merge import DeepMerge
from flask import g
from jinja2 import Template
from crc import db, app
from crc.api.common import ApiError
from crc.models.api_models import Task, MultiInstanceType, NavigationItem, NavigationItemSchema, WorkflowApi
from crc.models.api_models import Task, MultiInstanceType, WorkflowApi
from crc.models.file import LookupDataModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.task_event import TaskEventModel
from crc.models.user import UserModel, UserModelSchema
from crc.models.workflow import WorkflowModel, WorkflowStatus, WorkflowSpecModel
from crc.services.file_service import FileService
@ -98,6 +96,7 @@ class WorkflowService(object):
while not processor.bpmn_workflow.is_completed():
try:
processor.bpmn_workflow.get_deep_nav_list() # Assure no errors with navigation.
processor.bpmn_workflow.do_engine_steps()
tasks = processor.bpmn_workflow.get_tasks(SpiffTask.READY)
for task in tasks:
@ -116,6 +115,7 @@ class WorkflowService(object):
raise ApiError.from_workflow_exception("workflow_validation_exception", str(we), we)
WorkflowService.delete_test_data()
WorkflowService._process_documentation(processor.bpmn_workflow.last_task.parent.parent)
return processor.bpmn_workflow.last_task.data
@staticmethod
@ -127,6 +127,19 @@ class WorkflowService(object):
form_data = task.data # Just like with the front end, we start with what was already there, and modify it.
hide_groups = []
for field in task_api.form.fields:
# Assure we have a field type
if field.type is None:
raise ApiError(code='invalid_form_data',
message = f'Type is missing for field "{field.id}". A field type must be provided.',
task_id = task.id,
task_name = task.get_name())
# Assure we have valid ids
if not WorkflowService.check_field_id(field.id):
raise ApiError(code='invalid_form_id',
message=f'Invalid Field name: "{field.id}". A field ID must begin with a letter, '
f'and can only contain letters, numbers, and "_"',
task_id = task.id,
task_name = task.get_name())
# Assure field has valid properties
WorkflowService.check_field_properties(field, task)
@ -175,6 +188,18 @@ class WorkflowService(object):
task.data = {}
task.data.update(form_data)
@staticmethod
def check_field_id(id):
"""Assures that field names are valid Python and Javascript names."""
if not id[0].isalpha():
return False
for char in id[1:len(id)]:
if char.isalnum() or char == '_':
pass
else:
return False
return True
@staticmethod
def check_field_properties(field, task):
"""Assures that all properties are valid on a given workflow."""
@ -320,33 +345,9 @@ class WorkflowService(object):
"""Returns an API model representing the state of the current workflow, if requested, and
possible, next_task is set to the current_task."""
nav_dict = processor.bpmn_workflow.get_nav_list()
navigation = processor.bpmn_workflow.get_deep_nav_list()
WorkflowService.update_navigation(navigation, processor)
# Some basic cleanup of the title for the for the navigation.
navigation = []
for nav_item in nav_dict:
spiff_task = processor.bpmn_workflow.get_task(nav_item['task_id'])
if 'description' in nav_item:
nav_item['title'] = nav_item.pop('description')
# fixme: duplicate code from the workflow_service. Should only do this in one place.
if nav_item['title'] is not None and ' ' in nav_item['title']:
nav_item['title'] = nav_item['title'].partition(' ')[2]
else:
nav_item['title'] = ""
if spiff_task:
nav_item['task'] = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
nav_item['title'] = nav_item['task'].title # Prefer the task title.
user_uids = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if not UserService.in_list(user_uids, allow_admin_impersonate=True):
nav_item['state'] = WorkflowService.TASK_STATE_LOCKED
else:
nav_item['task'] = None
navigation.append(NavigationItem(**nav_item))
NavigationItemSchema().dump(nav_item)
spec = db.session.query(WorkflowSpecModel).filter_by(id=processor.workflow_spec_id).first()
workflow_api = WorkflowApi(
@ -375,6 +376,29 @@ class WorkflowService(object):
workflow_api.next_task.state = WorkflowService.TASK_STATE_LOCKED
return workflow_api
@staticmethod
def update_navigation(navigation: List[NavItem], processor: WorkflowProcessor):
# Recursive function to walk down through children, and clean up descriptions, and statuses
for nav_item in navigation:
spiff_task = processor.bpmn_workflow.get_task(nav_item.task_id)
if spiff_task:
# Use existing logic to set the description, and alter the state based on permissions.
api_task = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
nav_item.description = api_task.title
user_uids = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if (isinstance(spiff_task.task_spec, UserTask) or isinstance(spiff_task.task_spec, ManualTask)) \
and not UserService.in_list(user_uids, allow_admin_impersonate=True):
nav_item.state = WorkflowService.TASK_STATE_LOCKED
else:
# Strip off the first word in the description, to meet guidlines for BPMN.
if nav_item.description:
if nav_item.description is not None and ' ' in nav_item.description:
nav_item.description = nav_item.description.partition(' ')[2]
# Recurse here
WorkflowService.update_navigation(nav_item.children, processor)
@staticmethod
def get_previously_submitted_data(workflow_id, spiff_task):
""" If the user has completed this task previously, find the form data for the last submission."""
@ -402,6 +426,7 @@ class WorkflowService(object):
return {}
@staticmethod
def spiff_task_to_api_task(spiff_task, add_docs_and_forms=False):
task_type = spiff_task.task_spec.__class__.__name__
@ -561,6 +586,13 @@ class WorkflowService(object):
items = data_model.items() if isinstance(data_model, dict) else data_model
options = []
for item in items:
if value_column not in item:
raise ApiError.from_task("invalid_enum", f"The value column '{value_column}' does not exist for item {item}",
task=spiff_task)
if label_column not in item:
raise ApiError.from_task("invalid_enum", f"The label column '{label_column}' does not exist for item {item}",
task=spiff_task)
options.append({"id": item[value_column], "name": item[label_column], "data": item})
return options

View File

@ -1,11 +1,12 @@
<?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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_06pyjz2" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_01143nb" name="PI&#39;s Pr" isExecutable="true">
<bpmn:process id="UserTask_ShowInvalidUIDs" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0kcrx5l</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="ScriptTask_LoadPersonnel" name="Load IRB Personnel">
<bpmn:incoming>Flow_0kcrx5l</bpmn:incoming>
<bpmn:incoming>Flow_00zanzw</bpmn:incoming>
<bpmn:outgoing>Flow_1dcsioh</bpmn:outgoing>
<bpmn:script>current_user = ldap()
investigators = study_info('investigators')
@ -15,11 +16,11 @@ is_cu_pi = False
if pi != None:
hasPI = True
if pi.get('uid', None) != None:
pi_has_uid = True
pi_invalid_uid = False
if pi['uid'] == current_user['uid']:
is_cu_pi = True
else:
pi_has_uid = False
pi_invalid_uid = True
else:
hasPI = False
@ -27,9 +28,11 @@ else:
dc = investigators.get('DEPT_CH', None)
if dc != None:
if dc.get('uid', None) != None:
dc_has_uid = True
dc_invalid_uid = False
else:
dc_has_uid = False
dc_invalid_uid = True
else:
dc_invalid_uid = False
# Primary Coordinators
pcs = {}
@ -39,13 +42,19 @@ for k in investigators.keys():
if k in ['SC_I','SC_II','IRBC']:
investigator = investigators.get(k)
if investigator.get('uid', None) != None:
cnt_pcs_uid = cnt_pcs_uid + 1
if investigator['uid'] != current_user['uid']:
pcs[k] = investigator
cnt_pcs_uid = cnt_pcs_uid + 1
else:
is_cu_pc = True
is_cu_pc_role = investigator['label']
else:
pcs[k] = investigator
cnt_pcs = len(pcs.keys())
if cnt_pcs != cnt_pcs_uid:
pcs_invalid_uid = True
else:
pcs_invalid_uid = False
if cnt_pcs &gt; 0:
del(k)
del(investigator)
@ -58,13 +67,19 @@ for k in investigators.keys():
if k == 'AS_C':
investigator = investigators.get(k)
if investigator.get('uid', None) != None:
cnt_acs_uid = cnt_acs_uid + 1
if investigator['uid'] != current_user['uid']:
acs[k] = investigator
cnt_acs_uid = cnt_acs_uid + 1
else:
is_cu_ac = True
is_cu_ac_role = investigator['label']
else:
acs[k] = investigator
cnt_acs = len(acs.keys())
if cnt_pcs != cnt_pcs_uid:
acs_invalid_uid = True
else:
acs_invalid_uid = False
if cnt_acs &gt; 0:
del(k)
del(investigator)
@ -77,12 +92,18 @@ for k in investigators.keys():
if k[:2] == 'SI':
investigator = investigators.get(k)
if investigator.get('uid', None) != None:
cnt_subs_uid = cnt_subs_uid + 1
if investigator['uid'] != current_user['uid']:
subs[k] = investigator
cnt_subs_uid = cnt_subs_uid + 1
else:
is_cu_subs = True
else:
subs[k] = investigator
cnt_subs = len(subs.keys())
if cnt_subs != cnt_subs_uid:
subs_invalid_uid = True
else:
subs_invalid_uid = False
if cnt_subs &gt; 0:
del(k)
del(investigator)
@ -95,13 +116,19 @@ for k in investigators.keys():
if k in ['SCI','DC']:
investigator = investigators.get(k)
if investigator.get('uid', None) != None:
cnt_aps_uid = cnt_aps_uid + 1
if investigator['uid'] != current_user['uid']:
aps[k] = investigator
cnt_aps_uid = cnt_aps_uid + 1
else:
is_cu_ap = True
is_cu_ap_role = investigator['label']
else:
aps[k] = investigator
cnt_aps = len(aps.keys())
if cnt_aps != cnt_aps_uid:
aps_invalid_uid = True
else:
aps_invalid_uid = False
if cnt_aps &gt; 0:
del(k)
del(investigator)
@ -132,12 +159,12 @@ Since you are the person entering this information, you already have access and
<camunda:property id="rows" value="5" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="pi.access" label="Should the Principal Investigator have full editing access in the system?" type="boolean" defaultValue="true">
<camunda:formField id="pi.access" label="Should the Principal Investigator have full editing access in the system?" type="boolean" defaultValue="True">
<camunda:properties>
<camunda:property id="hide_expression" value="is_cu_pi" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="pi.emails" label="Should the Principal Investigator receive automated email notifications?" type="boolean" defaultValue="true">
<camunda:formField id="pi.emails" label="Should the Principal Investigator receive automated email notifications?" type="boolean" defaultValue="True">
<camunda:properties>
<camunda:property id="hide_expression" value="is_cu_pi" />
</camunda:properties>
@ -160,23 +187,23 @@ Since you are the person entering this information, you already have access and
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0kcrx5l" sourceRef="StartEvent_1" targetRef="ScriptTask_LoadPersonnel" />
<bpmn:sequenceFlow id="Flow_1dcsioh" sourceRef="ScriptTask_LoadPersonnel" targetRef="Gateway_CheckForPI" />
<bpmn:exclusiveGateway id="Gateway_CheckForPI" name="PI Cnt" default="Flow_147b9li">
<bpmn:exclusiveGateway id="Gateway_CheckForPI" name="PI With Valid UID?" default="Flow_147b9li">
<bpmn:incoming>Flow_1dcsioh</bpmn:incoming>
<bpmn:outgoing>Flow_147b9li</bpmn:outgoing>
<bpmn:outgoing>Flow_00prawo</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_147b9li" name="1 PI from PB" sourceRef="Gateway_CheckForPI" targetRef="ScriptTask_DeterminePI_E0_Department" />
<bpmn:sequenceFlow id="Flow_00prawo" name="No PI from PB" sourceRef="Gateway_CheckForPI" targetRef="Activity_1qwzwyi">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">not(hasPI) or (hasPI and not(pi_has_uid))</bpmn:conditionExpression>
<bpmn:sequenceFlow id="Flow_147b9li" name="Yes" sourceRef="Gateway_CheckForPI" targetRef="Gateway_CheckUIDs" />
<bpmn:sequenceFlow id="Flow_00prawo" name="No" sourceRef="Gateway_CheckForPI" targetRef="Activity_1qwzwyi">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">not(hasPI) or (hasPI and pi_invalid_uid)</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:manualTask id="Activity_1qwzwyi" name="Show No PI">
<bpmn:manualTask id="Activity_1qwzwyi" name="Show No PI or Invalid UID">
<bpmn:documentation>No PI entered in PB</bpmn:documentation>
<bpmn:incoming>Flow_00prawo</bpmn:incoming>
<bpmn:outgoing>Flow_16qr5jf</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:exclusiveGateway id="Gateway_0jykh6r" name="How many Primary Coordinators?" default="Flow_0xifvai">
<bpmn:incoming>Flow_0kpe12r</bpmn:incoming>
<bpmn:incoming>SequenceFlow_0cdtt11</bpmn:incoming>
<bpmn:incoming>Flow_1ayisx2</bpmn:incoming>
<bpmn:outgoing>Flow_0xifvai</bpmn:outgoing>
<bpmn:outgoing>Flow_1oqem42</bpmn:outgoing>
</bpmn:exclusiveGateway>
@ -210,7 +237,8 @@ Otherwise, edit each Coordinator as necessary and select the Save button for eac
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">cnt_pcs == 0</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:scriptTask id="ScriptTask_DeterminePI_E0_Department" name="Determine PI E0 Department">
<bpmn:incoming>Flow_147b9li</bpmn:incoming>
<bpmn:incoming>Flow_0tfprc8</bpmn:incoming>
<bpmn:incoming>Flow_0tsdclr</bpmn:incoming>
<bpmn:outgoing>Flow_1grahhv</bpmn:outgoing>
<bpmn:script>LDAP_dept = pi.department
length_LDAP_dept = len(LDAP_dept)
@ -274,36 +302,12 @@ else:
<bpmn:incoming>Flow_0w4d2bz</bpmn:incoming>
<bpmn:outgoing>Flow_1oo0ijr</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:userTask id="UserTask_109otvi" name="Update Chair Info" camunda:formKey="RO_Chair_Info">
<bpmn:documentation>***Name &amp; Degree:*** {{ RO_Chair_Name_Degree }}
***School:*** {{ RO_School }}
***Department:*** {{ RO_Department }}
***Title:*** {{ RO_Chair_Title }}
***Email:*** {{ RO_Chair_CID }}
{% if RO_Chair_CID != dc.uid %}
*Does not match the Department Chair specified in Protocol Builder, {{ dc.display_name }}*
{% endif %}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="RO_ChairAccess" label="Should the Department Chair have full editing access in the system?" type="boolean" defaultValue="false" />
<camunda:formField id="RO_ChairEmails" label="Should the Department Chair receive automated email notifications?" type="boolean" defaultValue="false" />
</camunda:formData>
<camunda:properties>
<camunda:property name="display_name" value="&#34;Responsible Organization&#39;s Chair Info&#34;" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0vi6thu</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0cdtt11</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_0cdtt11" sourceRef="UserTask_109otvi" targetRef="Gateway_0jykh6r" />
<bpmn:exclusiveGateway id="Gateway_PI_is_DeptChair" name="PI is Dept Chair?" default="Flow_0vi6thu">
<bpmn:incoming>Flow_070j5fg</bpmn:incoming>
<bpmn:outgoing>Flow_0vi6thu</bpmn:outgoing>
<bpmn:outgoing>Flow_00yhlrq</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_0vi6thu" name="No" sourceRef="Gateway_PI_is_DeptChair" targetRef="UserTask_109otvi" />
<bpmn:sequenceFlow id="Flow_0vi6thu" name="No" sourceRef="Gateway_PI_is_DeptChair" targetRef="Activity_1sffono" />
<bpmn:sequenceFlow id="Flow_00yhlrq" name="Yes" sourceRef="Gateway_PI_is_DeptChair" targetRef="Activity_ShowPI_is_DeptChair">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">RO_Chair_CID == pi.uid</bpmn:conditionExpression>
</bpmn:sequenceFlow>
@ -360,6 +364,7 @@ Otherwise, edit each Sub-Investigator as necessary and select the Save button fo
<bpmn:outgoing>Flow_1kg5jot</bpmn:outgoing>
<bpmn:script>pi.E0.schoolName = PI_E0_schoolName
pi.E0.deptName = PI_E0_deptName
pi.experience = user_data_get("pi_experience","")
ro = {}
ro['chair'] = {}</bpmn:script>
</bpmn:scriptTask>
@ -605,17 +610,17 @@ ro.schoolAbbrv = RO_StudySchool.value</bpmn:script>
<bpmn:outgoing>Flow_0vff9k5</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_0vff9k5" sourceRef="Gateway_0zd7syo" targetRef="BusinessRuleTask_Determine_RO_Chair" />
<bpmn:exclusiveGateway id="Gateway_13k761k" name="How many Additional Personnel? " default="Flow_0kp47dz">
<bpmn:exclusiveGateway id="Gateway_13k761k" name="How many Additional Personnel? " default="Flow_0q56tn8">
<bpmn:incoming>Flow_0ofpgml</bpmn:incoming>
<bpmn:incoming>Flow_0jxzqw1</bpmn:incoming>
<bpmn:outgoing>Flow_0q56tn8</bpmn:outgoing>
<bpmn:outgoing>Flow_0kp47dz</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_0q56tn8" sourceRef="Gateway_13k761k" targetRef="Activity_1sra1vn">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">cnt_aps &gt; 0</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0q56tn8" sourceRef="Gateway_13k761k" targetRef="Activity_1sra1vn" />
<bpmn:sequenceFlow id="Flow_10zn0h1" sourceRef="Activity_1sra1vn" targetRef="EndEvent_1qor16n" />
<bpmn:sequenceFlow id="Flow_0kp47dz" sourceRef="Gateway_13k761k" targetRef="EndEvent_1qor16n" />
<bpmn:sequenceFlow id="Flow_0kp47dz" sourceRef="Gateway_13k761k" targetRef="EndEvent_1qor16n">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">cnt_aps == 0</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_1sra1vn" name="Update Additional Personnel Info" camunda:formKey="AP_AccessEmails">
<bpmn:documentation>The following Additional Personnel were entered in Protocol Builder:
{%+ for key, value in aps.items() %}{{value.display_name}} ({{key}}){% if loop.index is lt cnt_aps %}, {% endif %}{% endfor %}
@ -646,400 +651,545 @@ Otherwise, edit each Additional Personnel as necessary and select the Save butto
<bpmn:outgoing>Flow_10zn0h1</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics camunda:collection="aps" camunda:elementVariable="ap" />
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_CheckUIDs" name="Invalid UIDs?" default="Flow_0tfprc8">
<bpmn:incoming>Flow_147b9li</bpmn:incoming>
<bpmn:outgoing>Flow_0tfprc8</bpmn:outgoing>
<bpmn:outgoing>Flow_0nz62mu</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_0tfprc8" name="No" sourceRef="Gateway_CheckUIDs" targetRef="ScriptTask_DeterminePI_E0_Department" />
<bpmn:sequenceFlow id="Flow_0nz62mu" name="Yes" sourceRef="Gateway_CheckUIDs" targetRef="Activity_19z6vct">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">dc_invalid_uid or pcs_invalid_uid or acs_invalid_uid or subs_invalid_uid or aps_invalid_uid</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_19z6vct" name="Show Invalid UIDs" camunda:formKey="ShowInvalidUIDs">
<bpmn:documentation>Select No if all displayed invalid Computing IDs do not need system access and/or receive emails. If they do, correct in Protocol Builder first and then select Yes.
{% if dc_invalid_uid %}
Department Chair
{{ dc.error }}
{% endif %}
{% if pcs_invalid_uid %}
Primary Coordinators
{% for k, pc in pcs.items() %}
{% if pc.get('uid', None) == None: %}
{{ pc.error }}
{% endif %}
{% endfor %}
{% endif %}
{% if acs_invalid_uid %}
Additional Coordinators
{% for k, ac in acs.items() %}
{% if ac.get('uid', None) == None: %}
{{ ac.error }}
{% endif %}
{% endfor %}
{% endif %}
{% if subs_invalid_uid %}
Sub-Investigators
{% for k, sub in subs.items() %}
{% if sub.get('uid', None) == None: %}
{{ sub.error }}
{% endif %}
{% endfor %}
{% endif %}
{% if aps_invalid_uid %}
Additional Personnnel
{% for k, ap in aps.items() %}
{% if ap.get('uid', None) == None: %}
{{ ap.error }}
{% endif %}
{% endfor %}
{% endif %}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="FixInvalidUIDs" label="Do you want to fix?" type="boolean">
<camunda:properties>
<camunda:property id="description" value="Select Yes if you have corrected in Protocol Builder, No if you would like to proceed without correcting." />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0nz62mu</bpmn:incoming>
<bpmn:outgoing>Flow_16bkbuc</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_FixInvalidUIDs" name="Fix Invalid UIDs?" default="Flow_00zanzw">
<bpmn:incoming>Flow_16bkbuc</bpmn:incoming>
<bpmn:outgoing>Flow_00zanzw</bpmn:outgoing>
<bpmn:outgoing>Flow_0tsdclr</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_16bkbuc" sourceRef="Activity_19z6vct" targetRef="Gateway_FixInvalidUIDs" />
<bpmn:sequenceFlow id="Flow_00zanzw" name="Yes" sourceRef="Gateway_FixInvalidUIDs" targetRef="ScriptTask_LoadPersonnel" />
<bpmn:sequenceFlow id="Flow_0tsdclr" name="No" sourceRef="Gateway_FixInvalidUIDs" targetRef="ScriptTask_DeterminePI_E0_Department">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">not(FixInvalidUIDs)</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:userTask id="Activity_1sffono" name="Update Chair Info" camunda:formKey="RO_Chair_Info">
<bpmn:documentation>***Name &amp; Degree:*** {{ RO_Chair_Name_Degree }}
***School:*** {{ RO_School }}
***Department:*** {{ RO_Department }}
***Title:*** {{ RO_Chair_Title }}
***Email:*** {{ RO_Chair_CID }}
{% if RO_Chair_CID != dc.uid %}
*Does not match the Department Chair specified in Protocol Builder, {{ dc.display_name }}*
{% endif %}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="RO_ChairAccess" label="Should the Department Chair have full editing access in the system?" type="boolean" defaultValue="false" />
<camunda:formField id="RO_ChairEmails" label="Should the Department Chair receive automated email notifications?" type="boolean" defaultValue="false" />
</camunda:formData>
<camunda:properties>
<camunda:property name="display_name" value="&#34;Responsible Organization&#39;s Chair Info&#34;" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0vi6thu</bpmn:incoming>
<bpmn:outgoing>Flow_1ayisx2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1ayisx2" sourceRef="Activity_1sffono" targetRef="Gateway_0jykh6r" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_01143nb">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="UserTask_ShowInvalidUIDs">
<bpmndi:BPMNEdge id="Flow_1ayisx2_di" bpmnElement="Flow_1ayisx2">
<di:waypoint x="2810" y="290" />
<di:waypoint x="2875" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0tsdclr_di" bpmnElement="Flow_0tsdclr">
<di:waypoint x="715" y="540" />
<di:waypoint x="860" y="540" />
<di:waypoint x="860" y="330" />
<bpmndi:BPMNLabel>
<dc:Bounds x="781" y="522" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_00zanzw_di" bpmnElement="Flow_00zanzw">
<di:waypoint x="690" y="565" />
<di:waypoint x="690" y="610" />
<di:waypoint x="360" y="610" />
<di:waypoint x="360" y="330" />
<bpmndi:BPMNLabel>
<dc:Bounds x="516" y="592" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16bkbuc_di" bpmnElement="Flow_16bkbuc">
<di:waypoint x="690" y="460" />
<di:waypoint x="690" y="515" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0nz62mu_di" bpmnElement="Flow_0nz62mu">
<di:waypoint x="690" y="315" />
<di:waypoint x="690" y="380" />
<bpmndi:BPMNLabel>
<dc:Bounds x="696" y="345" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0tfprc8_di" bpmnElement="Flow_0tfprc8">
<di:waypoint x="715" y="290" />
<di:waypoint x="810" y="290" />
<bpmndi:BPMNLabel>
<dc:Bounds x="756" y="272" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0kp47dz_di" bpmnElement="Flow_0kp47dz">
<di:waypoint x="3800" y="265" />
<di:waypoint x="3800" y="200" />
<di:waypoint x="4150" y="200" />
<di:waypoint x="4150" y="272" />
<di:waypoint x="3960" y="265" />
<di:waypoint x="3960" y="200" />
<di:waypoint x="4310" y="200" />
<di:waypoint x="4310" y="272" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_10zn0h1_di" bpmnElement="Flow_10zn0h1">
<di:waypoint x="4030" y="290" />
<di:waypoint x="4132" y="290" />
<di:waypoint x="4190" y="290" />
<di:waypoint x="4292" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0q56tn8_di" bpmnElement="Flow_0q56tn8">
<di:waypoint x="3825" y="290" />
<di:waypoint x="3930" y="290" />
<di:waypoint x="3985" y="290" />
<di:waypoint x="4090" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vff9k5_di" bpmnElement="Flow_0vff9k5">
<di:waypoint x="2040" y="375" />
<di:waypoint x="2040" y="330" />
<di:waypoint x="2200" y="375" />
<di:waypoint x="2200" y="330" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0iuzu7j_di" bpmnElement="Flow_0iuzu7j">
<di:waypoint x="2015" y="830" />
<di:waypoint x="1900" y="830" />
<di:waypoint x="1900" y="760" />
<di:waypoint x="2175" y="830" />
<di:waypoint x="2060" y="830" />
<di:waypoint x="2060" y="760" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1905" y="783" width="49" height="14" />
<dc:Bounds x="2065" y="783" width="49" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0giqf35_di" bpmnElement="Flow_0giqf35">
<di:waypoint x="2065" y="830" />
<di:waypoint x="2180" y="830" />
<di:waypoint x="2180" y="760" />
<di:waypoint x="2225" y="830" />
<di:waypoint x="2340" y="830" />
<di:waypoint x="2340" y="760" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2189" y="783" width="22" height="14" />
<dc:Bounds x="2349" y="783" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0mdjaid_di" bpmnElement="Flow_0mdjaid">
<di:waypoint x="2040" y="900" />
<di:waypoint x="2040" y="855" />
<di:waypoint x="2200" y="900" />
<di:waypoint x="2200" y="855" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vyg8ir_di" bpmnElement="Flow_1vyg8ir">
<di:waypoint x="2180" y="680" />
<di:waypoint x="2180" y="620" />
<di:waypoint x="2065" y="620" />
<di:waypoint x="2340" y="680" />
<di:waypoint x="2340" y="620" />
<di:waypoint x="2225" y="620" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0zc01f9_di" bpmnElement="Flow_0zc01f9">
<di:waypoint x="2040" y="680" />
<di:waypoint x="2040" y="645" />
<di:waypoint x="2200" y="680" />
<di:waypoint x="2200" y="645" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vv63qa_di" bpmnElement="Flow_1vv63qa">
<di:waypoint x="2040" y="460" />
<di:waypoint x="2040" y="425" />
<di:waypoint x="2200" y="460" />
<di:waypoint x="2200" y="425" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1azfvtx_di" bpmnElement="Flow_1azfvtx">
<di:waypoint x="2040" y="805" />
<di:waypoint x="2040" y="760" />
<di:waypoint x="2200" y="805" />
<di:waypoint x="2200" y="760" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2047" y="783" width="45" height="14" />
<dc:Bounds x="2207" y="783" width="45" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0w4d2bz_di" bpmnElement="Flow_0w4d2bz">
<di:waypoint x="1775" y="290" />
<di:waypoint x="1990" y="290" />
<di:waypoint x="1935" y="290" />
<di:waypoint x="2150" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1mplloa_di" bpmnElement="Flow_1mplloa">
<di:waypoint x="1460" y="290" />
<di:waypoint x="1540" y="290" />
<di:waypoint x="1620" y="290" />
<di:waypoint x="1700" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1va8c15_di" bpmnElement="Flow_1va8c15">
<di:waypoint x="1640" y="290" />
<di:waypoint x="1725" y="290" />
<di:waypoint x="1800" y="290" />
<di:waypoint x="1885" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0m9peiz_di" bpmnElement="Flow_0m9peiz">
<di:waypoint x="2040" y="595" />
<di:waypoint x="2040" y="540" />
<di:waypoint x="2200" y="595" />
<di:waypoint x="2200" y="540" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0fw4rck_di" bpmnElement="Flow_0fw4rck">
<di:waypoint x="2065" y="830" />
<di:waypoint x="2270" y="830" />
<di:waypoint x="2270" y="400" />
<di:waypoint x="2065" y="400" />
<di:waypoint x="2225" y="830" />
<di:waypoint x="2430" y="830" />
<di:waypoint x="2430" y="400" />
<di:waypoint x="2225" y="400" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2278" y="603" width="15" height="14" />
<dc:Bounds x="2438" y="603" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0whqr3p_di" bpmnElement="Flow_0whqr3p">
<di:waypoint x="1900" y="680" />
<di:waypoint x="1900" y="620" />
<di:waypoint x="2015" y="620" />
<di:waypoint x="2060" y="680" />
<di:waypoint x="2060" y="620" />
<di:waypoint x="2175" y="620" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1yz8k2a_di" bpmnElement="Flow_1yz8k2a">
<di:waypoint x="2040" y="1050" />
<di:waypoint x="2040" y="980" />
<di:waypoint x="2200" y="1050" />
<di:waypoint x="2200" y="980" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1fj9iz0_di" bpmnElement="Flow_1fj9iz0">
<di:waypoint x="1800" y="1090" />
<di:waypoint x="1990" y="1090" />
<di:waypoint x="1960" y="1090" />
<di:waypoint x="2150" y="1090" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ycdxbl_di" bpmnElement="Flow_0ycdxbl">
<di:waypoint x="1750" y="855" />
<di:waypoint x="1750" y="1050" />
<di:waypoint x="1910" y="855" />
<di:waypoint x="1910" y="1050" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1722" y="933" width="15" height="14" />
<dc:Bounds x="1882" y="933" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13la8l3_di" bpmnElement="Flow_13la8l3">
<di:waypoint x="1775" y="830" />
<di:waypoint x="2015" y="830" />
<di:waypoint x="1935" y="830" />
<di:waypoint x="2175" y="830" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1811" y="813" width="18" height="14" />
<dc:Bounds x="1971" y="813" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1yd7kbi_di" bpmnElement="Flow_1yd7kbi">
<di:waypoint x="1750" y="315" />
<di:waypoint x="1750" y="805" />
<di:waypoint x="1910" y="315" />
<di:waypoint x="1910" y="805" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1722" y="691" width="15" height="14" />
<dc:Bounds x="1882" y="691" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0dt3pjw_di" bpmnElement="Flow_0dt3pjw">
<di:waypoint x="3100" y="265" />
<di:waypoint x="3100" y="180" />
<di:waypoint x="3480" y="180" />
<di:waypoint x="3480" y="265" />
<di:waypoint x="3260" y="265" />
<di:waypoint x="3260" y="180" />
<di:waypoint x="3640" y="180" />
<di:waypoint x="3640" y="265" />
<bpmndi:BPMNLabel>
<dc:Bounds x="3277" y="162" width="27" height="14" />
<dc:Bounds x="3437" y="162" width="27" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_12ss6u8_di" bpmnElement="Flow_12ss6u8">
<di:waypoint x="3340" y="290" />
<di:waypoint x="3455" y="290" />
<di:waypoint x="3500" y="290" />
<di:waypoint x="3615" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gtl2o3_di" bpmnElement="Flow_1gtl2o3">
<di:waypoint x="3125" y="290" />
<di:waypoint x="3240" y="290" />
<di:waypoint x="3285" y="290" />
<di:waypoint x="3400" y="290" />
<bpmndi:BPMNLabel>
<dc:Bounds x="3159" y="272" width="48" height="14" />
<dc:Bounds x="3319" y="272" width="48" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_070j5fg_di" bpmnElement="Flow_070j5fg">
<di:waypoint x="2270" y="290" />
<di:waypoint x="2365" y="290" />
<di:waypoint x="2430" y="290" />
<di:waypoint x="2525" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1kg5jot_di" bpmnElement="Flow_1kg5jot">
<di:waypoint x="1280" y="290" />
<di:waypoint x="1360" y="290" />
<di:waypoint x="1440" y="290" />
<di:waypoint x="1520" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16qr5jf_di" bpmnElement="Flow_16qr5jf">
<di:waypoint x="580" y="410" />
<di:waypoint x="642" y="410" />
<di:waypoint x="740" y="150" />
<di:waypoint x="822" y="150" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0jxzqw1_di" bpmnElement="Flow_0jxzqw1">
<di:waypoint x="3480" y="315" />
<di:waypoint x="3480" y="390" />
<di:waypoint x="3800" y="390" />
<di:waypoint x="3800" y="315" />
<di:waypoint x="3640" y="315" />
<di:waypoint x="3640" y="390" />
<di:waypoint x="3960" y="390" />
<di:waypoint x="3960" y="315" />
<bpmndi:BPMNLabel>
<dc:Bounds x="3627" y="372" width="27" height="14" />
<dc:Bounds x="3787" y="372" width="27" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ofpgml_di" bpmnElement="Flow_0ofpgml">
<di:waypoint x="3710" y="290" />
<di:waypoint x="3775" y="290" />
<di:waypoint x="3870" y="290" />
<di:waypoint x="3935" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_05rqrlf_di" bpmnElement="Flow_05rqrlf">
<di:waypoint x="3505" y="290" />
<di:waypoint x="3610" y="290" />
<di:waypoint x="3665" y="290" />
<di:waypoint x="3770" y="290" />
<bpmndi:BPMNLabel>
<dc:Bounds x="3534" y="272" width="48" height="14" />
<dc:Bounds x="3694" y="272" width="48" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0kpe12r_di" bpmnElement="Flow_0kpe12r">
<di:waypoint x="2440" y="120" />
<di:waypoint x="2740" y="120" />
<di:waypoint x="2740" y="260" />
<di:waypoint x="2600" y="120" />
<di:waypoint x="2900" y="120" />
<di:waypoint x="2900" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_00yhlrq_di" bpmnElement="Flow_00yhlrq">
<di:waypoint x="2390" y="265" />
<di:waypoint x="2390" y="160" />
<di:waypoint x="2550" y="265" />
<di:waypoint x="2550" y="160" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2401" y="178" width="18" height="14" />
<dc:Bounds x="2561" y="178" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vi6thu_di" bpmnElement="Flow_0vi6thu">
<di:waypoint x="2415" y="290" />
<di:waypoint x="2540" y="290" />
<di:waypoint x="2575" y="290" />
<di:waypoint x="2710" y="290" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2471" y="272" width="15" height="14" />
<dc:Bounds x="2620" y="272" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0cdtt11_di" bpmnElement="SequenceFlow_0cdtt11">
<di:waypoint x="2640" y="290" />
<di:waypoint x="2715" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1oo0ijr_di" bpmnElement="Flow_1oo0ijr">
<di:waypoint x="2090" y="290" />
<di:waypoint x="2170" y="290" />
<di:waypoint x="2250" y="290" />
<di:waypoint x="2330" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1eaikyp_di" bpmnElement="Flow_1eaikyp">
<di:waypoint x="920" y="290" />
<di:waypoint x="1010" y="290" />
<di:waypoint x="1070" y="290" />
<di:waypoint x="1160" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1wz38hl_di" bpmnElement="Flow_1wz38hl">
<di:waypoint x="1110" y="290" />
<di:waypoint x="1180" y="290" />
<di:waypoint x="1260" y="290" />
<di:waypoint x="1340" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1grahhv_di" bpmnElement="Flow_1grahhv">
<di:waypoint x="750" y="290" />
<di:waypoint x="820" y="290" />
<di:waypoint x="910" y="290" />
<di:waypoint x="970" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1oqem42_di" bpmnElement="Flow_1oqem42">
<di:waypoint x="2740" y="315" />
<di:waypoint x="2740" y="400" />
<di:waypoint x="3100" y="400" />
<di:waypoint x="3100" y="315" />
<di:waypoint x="2900" y="315" />
<di:waypoint x="2900" y="400" />
<di:waypoint x="3260" y="400" />
<di:waypoint x="3260" y="315" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2916" y="383" width="27" height="14" />
<dc:Bounds x="3076" y="383" width="27" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1n0k4pd_di" bpmnElement="Flow_1n0k4pd">
<di:waypoint x="2980" y="290" />
<di:waypoint x="3075" y="290" />
<di:waypoint x="3140" y="290" />
<di:waypoint x="3235" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0xifvai_di" bpmnElement="Flow_0xifvai">
<di:waypoint x="2765" y="290" />
<di:waypoint x="2880" y="290" />
<di:waypoint x="2925" y="290" />
<di:waypoint x="3040" y="290" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2793" y="273" width="48" height="14" />
<dc:Bounds x="2953" y="273" width="48" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_00prawo_di" bpmnElement="Flow_00prawo">
<di:waypoint x="350" y="315" />
<di:waypoint x="350" y="410" />
<di:waypoint x="480" y="410" />
<di:waypoint x="510" y="265" />
<di:waypoint x="510" y="150" />
<di:waypoint x="640" y="150" />
<bpmndi:BPMNLabel>
<dc:Bounds x="359" y="353" width="71" height="14" />
<dc:Bounds x="483" y="204" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_147b9li_di" bpmnElement="Flow_147b9li">
<di:waypoint x="375" y="290" />
<di:waypoint x="650" y="290" />
<di:waypoint x="535" y="290" />
<di:waypoint x="665" y="290" />
<bpmndi:BPMNLabel>
<dc:Bounds x="402" y="273" width="63" height="14" />
<dc:Bounds x="583" y="273" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1dcsioh_di" bpmnElement="Flow_1dcsioh">
<di:waypoint x="250" y="290" />
<di:waypoint x="325" y="290" />
<di:waypoint x="410" y="290" />
<di:waypoint x="485" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0kcrx5l_di" bpmnElement="Flow_0kcrx5l">
<di:waypoint x="28" y="290" />
<di:waypoint x="150" y="290" />
<di:waypoint x="188" y="290" />
<di:waypoint x="310" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="-8" y="272" width="36" height="36" />
<dc:Bounds x="152" y="272" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_0h49cmf_di" bpmnElement="ScriptTask_LoadPersonnel">
<dc:Bounds x="150" y="250" width="100" height="80" />
<dc:Bounds x="310" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1qor16n_di" bpmnElement="EndEvent_1qor16n">
<dc:Bounds x="4132" y="272" width="36" height="36" />
<dc:Bounds x="4292" y="272" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="4140" y="318" width="20" height="14" />
<dc:Bounds x="4300" y="318" width="20" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0d622qi_di" bpmnElement="Activity_EditPI">
<dc:Bounds x="1360" y="250" width="100" height="80" />
<dc:Bounds x="1520" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0qzf1r3_di" bpmnElement="Gateway_CheckForPI" isMarkerVisible="true">
<dc:Bounds x="325" y="265" width="50" height="50" />
<dc:Bounds x="485" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="334" y="241" width="31" height="14" />
<dc:Bounds x="478" y="325" width="63" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0neg931_di" bpmnElement="Activity_1qwzwyi">
<dc:Bounds x="480" y="370" width="100" height="80" />
<dc:Bounds x="640" y="110" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0jykh6r_di" bpmnElement="Gateway_0jykh6r" isMarkerVisible="true">
<dc:Bounds x="2715" y="265" width="50" height="50" />
<dc:Bounds x="2875" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2745" y="309" width="70" height="40" />
<dc:Bounds x="2905" y="309" width="70" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1nz85vv_di" bpmnElement="TaskPMI_UpdateCoordinatorInfo">
<dc:Bounds x="2880" y="250" width="100" height="80" />
<dc:Bounds x="3040" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1z05bvn_di" bpmnElement="ScriptTask_DeterminePI_E0_Department">
<dc:Bounds x="650" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0uz6yhu_di" bpmnElement="BusinessRule_PI_Dept">
<dc:Bounds x="1010" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1sn7wxh_di" bpmnElement="BusinessRule_PI_School">
<dc:Bounds x="820" y="250" width="100" height="80" />
<dc:Bounds x="810" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1a7hck9_di" bpmnElement="UserTask_SelectChair">
<dc:Bounds x="1850" y="680" width="100" height="80" />
<dc:Bounds x="2010" y="680" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1sk9596_di" bpmnElement="BusinessRuleTask_Determine_RO_Chair">
<dc:Bounds x="1990" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_109otvi_di" bpmnElement="UserTask_109otvi">
<dc:Bounds x="2540" y="250" width="100" height="80" />
<dc:Bounds x="2150" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1xio5hy_di" bpmnElement="Gateway_PI_is_DeptChair" isMarkerVisible="true">
<dc:Bounds x="2365" y="265" width="50" height="50" />
<dc:Bounds x="2525" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2348" y="322" width="84" height="14" />
<dc:Bounds x="2508" y="322" width="84" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0i869dj_di" bpmnElement="Activity_ShowPI_is_DeptChair">
<dc:Bounds x="2340" y="80" width="100" height="80" />
<dc:Bounds x="2500" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1oxt6h1_di" bpmnElement="Gateway_1oxt6h1" isMarkerVisible="true">
<dc:Bounds x="3455" y="265" width="50" height="50" />
<dc:Bounds x="3615" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="3500" y="315" width="79" height="27" />
<dc:Bounds x="3660" y="315" width="79" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0oyqfs3_di" bpmnElement="Activity_0yd4wuz">
<dc:Bounds x="3610" y="250" width="100" height="80" />
<dc:Bounds x="3770" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0npjf2p_di" bpmnElement="Event_0npjf2p">
<dc:Bounds x="642" y="392" width="36" height="36" />
<dc:Bounds x="822" y="132" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_02led02_di" bpmnElement="ScriptTask_Update_PIData">
<dc:Bounds x="1180" y="250" width="100" height="80" />
<dc:Bounds x="1340" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1mt9o4o_di" bpmnElement="ScriptTask_UpdateRO_Data">
<dc:Bounds x="2170" y="250" width="100" height="80" />
<dc:Bounds x="2330" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0zrcknh_di" bpmnElement="Gateway_0zrcknh" isMarkerVisible="true">
<dc:Bounds x="3075" y="265" width="50" height="50" />
<dc:Bounds x="3235" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="3115" y="309" width="70" height="40" />
<dc:Bounds x="3275" y="309" width="70" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1gqvpu9_di" bpmnElement="Activity_1yjg742">
<dc:Bounds x="3240" y="250" width="100" height="80" />
<dc:Bounds x="3400" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_10ngpfu_di" bpmnElement="Gateway_10ngpfu" isMarkerVisible="true">
<dc:Bounds x="1725" y="265" width="50" height="50" />
<dc:Bounds x="1885" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1706" y="170" width="88" height="80" />
<dc:Bounds x="1866" y="170" width="88" height="80" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_141zszd_di" bpmnElement="Gateway_141zszd" isMarkerVisible="true">
<dc:Bounds x="1725" y="805" width="50" height="50" />
<dc:Bounds x="1885" y="805" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1644" y="823" width="72" height="14" />
<dc:Bounds x="1804" y="823" width="72" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0vjz7wg_di" bpmnElement="Activity_1h5mjwh">
<dc:Bounds x="1700" y="1050" width="100" height="80" />
<dc:Bounds x="1860" y="1050" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0evmxd3_di" bpmnElement="Activity_141w33n">
<dc:Bounds x="1990" y="1050" width="100" height="80" />
<dc:Bounds x="2150" y="1050" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1dfciuq_di" bpmnElement="Gateway_1dfciuq" isMarkerVisible="true">
<dc:Bounds x="2015" y="805" width="50" height="50" />
<dc:Bounds x="2175" y="805" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="2055" y="846" width="70" height="27" />
<dc:Bounds x="2215" y="846" width="70" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1gzewp9_di" bpmnElement="Gateway_1gzewp9" isMarkerVisible="true">
<dc:Bounds x="2015" y="595" width="50" height="50" />
<dc:Bounds x="2175" y="595" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zjabzm_di" bpmnElement="BusinessRuleTask_Determine_RO">
<dc:Bounds x="1990" y="460" width="100" height="80" />
<dc:Bounds x="2150" y="460" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_11muu5l_di" bpmnElement="Activity_11muu5l">
<dc:Bounds x="1990" y="680" width="100" height="80" />
<dc:Bounds x="2150" y="680" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_18f2rpz_di" bpmnElement="ScriptTask_SetRO_SchoolAndDept">
<dc:Bounds x="1700" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1bvro77_di" bpmnElement="Activity_1bvro77">
<dc:Bounds x="2130" y="680" width="100" height="80" />
<dc:Bounds x="2290" y="680" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1yen2d0_di" bpmnElement="Activity_1yen2d0">
<dc:Bounds x="1990" y="900" width="100" height="80" />
<dc:Bounds x="2150" y="900" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0zd7syo_di" bpmnElement="Gateway_0zd7syo" isMarkerVisible="true">
<dc:Bounds x="2015" y="375" width="50" height="50" />
<dc:Bounds x="2175" y="375" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_13k761k_di" bpmnElement="Gateway_13k761k" isMarkerVisible="true">
<dc:Bounds x="3775" y="265" width="50" height="50" />
<dc:Bounds x="3935" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="3822" y="309" width="56" height="40" />
<dc:Bounds x="3982" y="309" width="56" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1m6r78y_di" bpmnElement="Activity_1sra1vn">
<dc:Bounds x="3930" y="250" width="100" height="80" />
<dc:Bounds x="4090" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_18f2rpz_di" bpmnElement="ScriptTask_SetRO_SchoolAndDept">
<dc:Bounds x="1540" y="250" width="100" height="80" />
<bpmndi:BPMNShape id="Gateway_1gq2m4q_di" bpmnElement="Gateway_CheckUIDs" isMarkerVisible="true">
<dc:Bounds x="665" y="265" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="657" y="241" width="66" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1l5u9mj_di" bpmnElement="Activity_19z6vct">
<dc:Bounds x="640" y="380" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_01e55pl_di" bpmnElement="Gateway_FixInvalidUIDs" isMarkerVisible="true">
<dc:Bounds x="665" y="515" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="570" y="530" width="84" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1sffono_di" bpmnElement="Activity_1sffono">
<dc:Bounds x="2710" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1sn7wxh_di" bpmnElement="BusinessRule_PI_School">
<dc:Bounds x="970" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0uz6yhu_di" bpmnElement="BusinessRule_PI_Dept">
<dc:Bounds x="1160" y="250" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -4,6 +4,7 @@ import os
from crc import app, db, session
from crc.models.file import CONTENT_TYPES
from crc.models.ldap import LdapModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecCategoryModel
from crc.services.file_service import FileService
@ -21,8 +22,6 @@ class ExampleDataLoader:
def load_all(self):
self.load_reference_documents()
self.load_default_user()
categories = [
WorkflowSpecCategoryModel(
id=0,
@ -328,8 +327,10 @@ class ExampleDataLoader:
file.close()
def load_default_user(self):
user = UserModel(uid="xxx9x", email_address="xxx9x@virginia.edu", display_name="Development User")
user = UserModel(uid="dhf8r", email_address="dhf8r@virginia.edu", display_name="Development User")
ldap_info = LdapModel(uid="dhf8r", email_address="dhf8r@virginia.edu", display_name="Development User")
db.session.add(user)
db.session.add(ldap_info)
db.session.commit()
def ldap(): return "x";

View File

@ -350,18 +350,30 @@ class BaseTest(unittest.TestCase):
session.commit()
return approval
def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False, do_engine_steps=True, user_uid="dhf8r"):
user = session.query(UserModel).filter_by(uid=user_uid).first()
self.assertIsNotNone(user)
rv = self.app.get(f'/v1.0/workflow/{workflow.id}'
f'?soft_reset={str(soft_reset)}'
f'&hard_reset={str(hard_reset)}'
f'&do_engine_steps={str(do_engine_steps)}',
def get_workflow_common(self, url, user):
rv = self.app.get(url,
headers=self.logged_in_headers(user),
content_type="application/json")
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
workflow_api = WorkflowApiSchema().load(json_data)
return workflow_api
def get_workflow_api(self, workflow, do_engine_steps=True, user_uid="dhf8r"):
user = session.query(UserModel).filter_by(uid=user_uid).first()
self.assertIsNotNone(user)
url = (f'/v1.0/workflow/{workflow.id}'
f'?do_engine_steps={str(do_engine_steps)}')
workflow_api = self.get_workflow_common(url, user)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
return workflow_api
def restart_workflow_api(self, workflow, clear_data=False, user_uid="dhf8r"):
user = session.query(UserModel).filter_by(uid=user_uid).first()
self.assertIsNotNone(user)
url = (f'/v1.0/workflow/{workflow.id}/restart'
f'?clear_data={str(clear_data)}')
workflow_api = self.get_workflow_common(url, user)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
return workflow_api

View File

@ -1,5 +1,5 @@
<?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.0">
<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>
@ -20,7 +20,7 @@ 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: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" />

View File

@ -0,0 +1,72 @@
<?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_bd39673" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_fe6205f" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0scd96e</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0scd96e" sourceRef="StartEvent_1" targetRef="Activity_EmailForm" />
<bpmn:userTask id="Activity_EmailForm" name="Email Form" camunda:formKey="email_form">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="email_address" label="Enter Email" type="string" defaultValue="dan@sartography.com">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0scd96e</bpmn:incoming>
<bpmn:outgoing>Flow_0c60gne</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0c60gne" sourceRef="Activity_EmailForm" targetRef="Activity_SendEmail" />
<bpmn:endEvent id="Event_EndEvent">
<bpmn:incoming>Flow_19fqvhc</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_19fqvhc" sourceRef="Activity_SendEmail" targetRef="Event_EndEvent" />
<bpmn:scriptTask id="Activity_SendEmail" name="Send Email">
<bpmn:documentation>Dear Person,
Thank you for using this email example.
I hope this makes sense.
Yours faithfully,
Dan</bpmn:documentation>
<bpmn:incoming>Flow_0c60gne</bpmn:incoming>
<bpmn:outgoing>Flow_19fqvhc</bpmn:outgoing>
<bpmn:script>subject = 'My Email Subject'
email(subject, email_address)</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_fe6205f">
<bpmndi:BPMNEdge id="Flow_19fqvhc_di" bpmnElement="Flow_19fqvhc">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c60gne_di" bpmnElement="Flow_0c60gne">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0scd96e_di" bpmnElement="Flow_0scd96e">
<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="Activity_0wqsfcj_di" bpmnElement="Activity_EmailForm">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1wh1xsj_di" bpmnElement="Event_EndEvent">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1ajacra_di" bpmnElement="Activity_SendEmail">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,52 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_886a64d" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_FailingWorkflow" name="Failing Workflow" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0cszvz2</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0cszvz2" sourceRef="StartEvent_1" targetRef="Activity_CallFailingScript" />
<bpmn:scriptTask id="Activity_CallFailingScript" name="Call Failing Script">
<bpmn:incoming>Flow_0cszvz2</bpmn:incoming>
<bpmn:outgoing>Flow_1l02umo</bpmn:outgoing>
<bpmn:script>failing_script()</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1l02umo" sourceRef="Activity_CallFailingScript" targetRef="Activity_PlaceHolder" />
<bpmn:scriptTask id="Activity_PlaceHolder" name="Place Holder">
<bpmn:incoming>Flow_1l02umo</bpmn:incoming>
<bpmn:outgoing>Flow_08zq7mf</bpmn:outgoing>
<bpmn:script>print('I am a placeholder.')</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_0k0yvmv">
<bpmn:incoming>Flow_08zq7mf</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_08zq7mf" sourceRef="Activity_PlaceHolder" targetRef="Event_0k0yvmv" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_FailingWorkflow">
<bpmndi:BPMNEdge id="Flow_0cszvz2_di" bpmnElement="Flow_0cszvz2">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1l02umo_di" bpmnElement="Flow_1l02umo">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_08zq7mf_di" bpmnElement="Flow_08zq7mf">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" 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="Activity_045fev7_di" bpmnElement="Activity_CallFailingScript">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0f3nusg_di" bpmnElement="Activity_PlaceHolder">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0k0yvmv_di" bpmnElement="Event_0k0yvmv">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -25,7 +25,7 @@
<camunda:formData>
<camunda:formField id="favorite_color" label="What is your favorite color?" type="string" defaultValue="Yellow" />
<camunda:formField id="quest" label="What is your quest?" type="string" defaultValue="To seek the holly Grail!" />
<camunda:formField id="swallow_speed" label="What is the air speed velocity of an unladen swallow?" defaultValue="About 24 miles per hour" />
<camunda:formField id="swallow_speed" label="What is the air speed velocity of an unladen swallow?" type="string" defaultValue="About 24 miles per hour" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0a7090c</bpmn:incoming>
@ -48,13 +48,7 @@ Your supervisor provided the following feedback:
{{feedback}}
You are all done! WARNING: If you go back and reanswer the questions it will create a new approval request.
</bpmn:documentation>
You are all done! WARNING: If you go back and reanswer the questions it will create a new approval request.</bpmn:documentation>
<bpmn:incoming>Flow_1g38q6b</bpmn:incoming>
</bpmn:endEvent>
<bpmn:manualTask id="Activity_19ccxoj" name="Review Feedback">
@ -68,13 +62,7 @@ Your Supervisor provided the following feedback:
{{feedback}}
Please press save to re-try the questions, and submit your responses again.
</bpmn:documentation>
Please press save to re-try the questions, and submit your responses again.</bpmn:documentation>
<bpmn:incoming>Flow_0vnghsi</bpmn:incoming>
<bpmn:outgoing>Flow_070gq5r</bpmn:outgoing>
</bpmn:manualTask>
@ -108,29 +96,21 @@ Please press save to re-try the questions, and submit your responses again.
<bpmndi:BPMNShape id="Participant_0ozb2sp_di" bpmnElement="Participant_0ozb2sp" isHorizontal="true">
<dc:Bounds x="190" y="80" width="550" height="370" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1s1s7a1_di" bpmnElement="Lane_1s1s7a1" isHorizontal="true">
<dc:Bounds x="220" y="80" width="520" height="245" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1m47545_di" bpmnElement="Lane_1m47545" isHorizontal="true">
<dc:Bounds x="220" y="325" width="520" height="125" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1ys83yq_di" bpmnElement="TextAnnotation_1ys83yq">
<dc:Bounds x="250" y="100" width="130.6238034460753" height="68.28334396936822" />
<bpmndi:BPMNShape id="Lane_1s1s7a1_di" bpmnElement="Lane_1s1s7a1" isHorizontal="true">
<dc:Bounds x="220" y="80" width="520" height="245" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0a7090c_di" bpmnElement="Flow_0a7090c">
<di:waypoint x="276" y="260" />
<di:waypoint x="330" y="260" />
<bpmndi:BPMNEdge id="Flow_070gq5r_di" bpmnElement="Flow_070gq5r">
<di:waypoint x="510" y="220" />
<di:waypoint x="510" y="160" />
<di:waypoint x="380" y="160" />
<di:waypoint x="380" y="220" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gp4zfd_di" bpmnElement="Flow_1gp4zfd">
<di:waypoint x="430" y="390" />
<di:waypoint x="485" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vnghsi_di" bpmnElement="Flow_0vnghsi">
<di:waypoint x="510" y="365" />
<di:waypoint x="510" y="300" />
<bpmndi:BPMNLabel>
<dc:Bounds x="520" y="334" width="40" height="14" />
</bpmndi:BPMNLabel>
<bpmndi:BPMNEdge id="Flow_1hcpt7c_di" bpmnElement="Flow_1hcpt7c">
<di:waypoint x="380" y="300" />
<di:waypoint x="380" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1g38q6b_di" bpmnElement="Flow_1g38q6b">
<di:waypoint x="535" y="390" />
@ -140,15 +120,20 @@ Please press save to re-try the questions, and submit your responses again.
<dc:Bounds x="585" y="372" width="46" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hcpt7c_di" bpmnElement="Flow_1hcpt7c">
<di:waypoint x="380" y="300" />
<di:waypoint x="380" y="350" />
<bpmndi:BPMNEdge id="Flow_0vnghsi_di" bpmnElement="Flow_0vnghsi">
<di:waypoint x="510" y="365" />
<di:waypoint x="510" y="300" />
<bpmndi:BPMNLabel>
<dc:Bounds x="520" y="334" width="40" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_070gq5r_di" bpmnElement="Flow_070gq5r">
<di:waypoint x="510" y="220" />
<di:waypoint x="510" y="160" />
<di:waypoint x="380" y="160" />
<di:waypoint x="380" y="220" />
<bpmndi:BPMNEdge id="Flow_1gp4zfd_di" bpmnElement="Flow_1gp4zfd">
<di:waypoint x="430" y="390" />
<di:waypoint x="485" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0a7090c_di" bpmnElement="Flow_0a7090c">
<di:waypoint x="276" y="260" />
<di:waypoint x="330" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="240" y="242" width="36" height="36" />
@ -168,6 +153,9 @@ Please press save to re-try the questions, and submit your responses again.
<bpmndi:BPMNShape id="Activity_0zc7cgy_di" bpmnElement="Activity_14eor1x">
<dc:Bounds x="330" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1ys83yq_di" bpmnElement="TextAnnotation_1ys83yq">
<dc:Bounds x="250" y="100" width="130.6238034460753" height="68.28334396936822" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1kcb9ou_di" bpmnElement="Association_1kcb9ou">
<di:waypoint x="359" y="220" />
<di:waypoint x="333" y="168" />

View File

@ -1,5 +1,5 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<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:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0exnnpv" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1nfe5m9</bpmn:outgoing>
@ -12,7 +12,7 @@
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1bqiin0" sourceRef="Task_Script_Load_Study_Sponsors" targetRef="Activity_0cm6tn2" />
<bpmn:endEvent id="EndEvent_171dj09">
<bpmn:incoming>Flow_05136ua</bpmn:incoming>
<bpmn:incoming>Flow_1efanns</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_09cika8" sourceRef="Activity_0cm6tn2" targetRef="Activity_0d8iftx" />
<bpmn:scriptTask id="Activity_0cm6tn2" name="setval">
@ -37,7 +37,19 @@
<bpmn:outgoing>Flow_05136ua</bpmn:outgoing>
<bpmn:script>empty = user_data_get('testme','empty')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_05136ua" sourceRef="Activity_0xw717o" targetRef="EndEvent_171dj09" />
<bpmn:sequenceFlow id="Flow_05136ua" sourceRef="Activity_0xw717o" targetRef="Gateway_06osfqz" />
<bpmn:exclusiveGateway id="Gateway_06osfqz" default="Flow_00s638e">
<bpmn:incoming>Flow_05136ua</bpmn:incoming>
<bpmn:outgoing>Flow_1efanns</bpmn:outgoing>
<bpmn:outgoing>Flow_00s638e</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_1efanns" name="test flow expression" sourceRef="Gateway_06osfqz" targetRef="EndEvent_171dj09">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">user_data_get('nothing','default') == 'default'</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:endEvent id="Event_1yebjqg">
<bpmn:incoming>Flow_00s638e</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_00s638e" sourceRef="Gateway_06osfqz" targetRef="Event_1yebjqg" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0exnnpv">
@ -84,7 +96,24 @@
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_05136ua_di" bpmnElement="Flow_05136ua">
<di:waypoint x="810" y="330" />
<di:waypoint x="810" y="385" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Gateway_06osfqz_di" bpmnElement="Gateway_06osfqz" isMarkerVisible="true">
<dc:Bounds x="785" y="385" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1efanns_di" bpmnElement="Flow_1efanns">
<di:waypoint x="810" y="435" />
<di:waypoint x="810" y="672" />
<bpmndi:BPMNLabel>
<dc:Bounds x="798" y="551" width="54" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Event_1yebjqg_di" bpmnElement="Event_1yebjqg">
<dc:Bounds x="1012" y="392" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_00s638e_di" bpmnElement="Flow_00s638e">
<di:waypoint x="835" y="410" />
<di:waypoint x="1012" y="410" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -0,0 +1,98 @@
<?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:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_00j2iu5" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_1gmf4la" isExecutable="true">
<bpmn:documentation />
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1fmyo77</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="ScriptTask_02924vs" name="Load IRB Details">
<bpmn:incoming>SequenceFlow_1fmyo77</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_18nr0gf</bpmn:outgoing>
<bpmn:script>details = study_info('details')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1fmyo77" sourceRef="StartEvent_1" targetRef="ScriptTask_02924vs" />
<bpmn:sequenceFlow id="SequenceFlow_18nr0gf" sourceRef="ScriptTask_02924vs" targetRef="Activity_FromIRB-API" />
<bpmn:endEvent id="EndEvent_1qvyxg7">
<bpmn:documentation>| Data Point | Value | Help |
|:-------------- |:-------- |:------ |
{% for key, value in details.items() -%}
| {{key}} | {%- if value == None -%}
\-\-
{%- else -%}
{%- if value is number -%}
{%- if value = 1 -%}
True
{%- elif value == 0 -%}
False
{%- else -%}
{{value}}
{%- endif -%}
{%- elif value is string -%}
{%- if value|length -%}
{{value}}
{%- else -%}
Question not presented
{%- endif -%}
{%- endif -%}
{%- endif -%} | [Context here](/help)
{% endfor -%}</bpmn:documentation>
<bpmn:incoming>Flow_0m7unlb</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0m7unlb" sourceRef="Activity_FromIRB-API" targetRef="EndEvent_1qvyxg7" />
<bpmn:manualTask id="Activity_FromIRB-API" name="From IRB API">
<bpmn:documentation>| Data Point | Value | Help |
|:-------------- |:-------- |:------ |
{% for key, value in details.items() -%}
| {{key}} | {%- if value == None -%}
\-\-
{%- else -%}
{%- if value is number -%}
{%- if value == 1 -%}
True
{%- elif value == 0 -%}
False
{%- else -%}
{{value}}
{%- endif -%}
{%- elif value is string -%}
{%- if value|length -%}
{{value}}
{%- else -%}
Question not presented
{%- endif -%}
{%- endif -%}
{%- endif -%} | [Context here](/help)
{% endfor -%}</bpmn:documentation>
<bpmn:incoming>SequenceFlow_18nr0gf</bpmn:incoming>
<bpmn:outgoing>Flow_0m7unlb</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1gmf4la">
<bpmndi:BPMNEdge id="Flow_0m7unlb_di" bpmnElement="Flow_0m7unlb">
<di:waypoint x="520" y="117" />
<di:waypoint x="622" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_18nr0gf_di" bpmnElement="SequenceFlow_18nr0gf">
<di:waypoint x="360" y="117" />
<di:waypoint x="420" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1fmyo77_di" bpmnElement="SequenceFlow_1fmyo77">
<di:waypoint x="188" y="117" />
<di:waypoint x="260" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_02924vs_di" bpmnElement="ScriptTask_02924vs">
<dc:Bounds x="260" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1qvyxg7_di" bpmnElement="EndEvent_1qvyxg7">
<dc:Bounds x="622" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_19nawos_di" bpmnElement="Activity_FromIRB-API">
<dc:Bounds x="420" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,86 @@
<?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_06dpn07" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_1iqn8uk" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0dbfi6t</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0dbfi6t" sourceRef="StartEvent_1" targetRef="Activity_09rr8u7" />
<bpmn:manualTask id="Activity_09rr8u7" name="Hello">
<bpmn:documentation>&lt;H1&gt;Hello&lt;/H1&gt;</bpmn:documentation>
<bpmn:incoming>Flow_0dbfi6t</bpmn:incoming>
<bpmn:outgoing>Flow_02rje6r</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_02rje6r" sourceRef="Activity_09rr8u7" targetRef="Activity_GetName" />
<bpmn:userTask id="Activity_GetName" name="Get Name" camunda:formKey="GetName">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="name" label="Name" type="string" defaultValue="World" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_02rje6r</bpmn:incoming>
<bpmn:outgoing>Flow_1iphrck</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1iphrck" sourceRef="Activity_GetName" targetRef="Activity_GetTitle" />
<bpmn:userTask id="Activity_GetTitle" name="Get Title" camunda:formKey="GetTitle">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="user-title" label="Title" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1iphrck</bpmn:incoming>
<bpmn:outgoing>Flow_0cxh51h</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0cxh51h" sourceRef="Activity_GetTitle" targetRef="Activity_SayHello" />
<bpmn:scriptTask id="Activity_SayHello" name="Say Hello">
<bpmn:incoming>Flow_0cxh51h</bpmn:incoming>
<bpmn:outgoing>Flow_0hbiuz4</bpmn:outgoing>
<bpmn:script>print('Hello', name)</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_13veu8t">
<bpmn:incoming>Flow_0hbiuz4</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0hbiuz4" sourceRef="Activity_SayHello" targetRef="Event_13veu8t" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1iqn8uk">
<bpmndi:BPMNEdge id="Flow_0dbfi6t_di" bpmnElement="Flow_0dbfi6t">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_02rje6r_di" bpmnElement="Flow_02rje6r">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1iphrck_di" bpmnElement="Flow_1iphrck">
<di:waypoint x="530" y="117" />
<di:waypoint x="590" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0cxh51h_di" bpmnElement="Flow_0cxh51h">
<di:waypoint x="690" y="117" />
<di:waypoint x="750" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0hbiuz4_di" bpmnElement="Flow_0hbiuz4">
<di:waypoint x="850" y="117" />
<di:waypoint x="912" 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="Activity_132twgr_di" bpmnElement="Activity_09rr8u7">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0it9qzi_di" bpmnElement="Activity_GetName">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_19s9l3h_di" bpmnElement="Activity_GetTitle">
<dc:Bounds x="590" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_05qpklh_di" bpmnElement="Activity_SayHello">
<dc:Bounds x="750" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_13veu8t_di" bpmnElement="Event_13veu8t">
<dc:Bounds x="912" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,72 @@
<?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_a699b4a" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_PrintName" name="PrintName" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1wfzn0v</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1wfzn0v" sourceRef="StartEvent_1" targetRef="Activity_GetName" />
<bpmn:sequenceFlow id="Flow_0e9yohi" sourceRef="Activity_GetName" targetRef="Activity_PrintHello" />
<bpmn:sequenceFlow id="Flow_1nt2lx5" sourceRef="Activity_PrintHello" targetRef="Activity_ThankYou" />
<bpmn:endEvent id="Event_EndEvent" name="End Event">
<bpmn:incoming>Flow_1yrn6kp</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1yrn6kp" sourceRef="Activity_ThankYou" targetRef="Event_EndEvent" />
<bpmn:userTask id="Activity_GetName" name="Get Name" camunda:formKey="GetNameForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="name" label="Name: " defaultValue="World" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1wfzn0v</bpmn:incoming>
<bpmn:outgoing>Flow_0e9yohi</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Activity_PrintHello" name="Print Hello">
<bpmn:incoming>Flow_0e9yohi</bpmn:incoming>
<bpmn:outgoing>Flow_1nt2lx5</bpmn:outgoing>
<bpmn:script>print('Hello', name)</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_ThankYou" name="Thank You">
<bpmn:incoming>Flow_1nt2lx5</bpmn:incoming>
<bpmn:outgoing>Flow_1yrn6kp</bpmn:outgoing>
<bpmn:script>print('Thank You')</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_PrintName">
<bpmndi:BPMNEdge id="Flow_1yrn6kp_di" bpmnElement="Flow_1yrn6kp">
<di:waypoint x="690" y="177" />
<di:waypoint x="752" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1nt2lx5_di" bpmnElement="Flow_1nt2lx5">
<di:waypoint x="530" y="177" />
<di:waypoint x="590" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0e9yohi_di" bpmnElement="Flow_0e9yohi">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1wfzn0v_di" bpmnElement="Flow_1wfzn0v">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_04bt6qi_di" bpmnElement="Event_EndEvent">
<dc:Bounds x="752" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="745" y="202" width="51" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_170ilqg_di" bpmnElement="Activity_GetName">
<dc:Bounds x="270" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_09hu4va_di" bpmnElement="Activity_PrintHello">
<dc:Bounds x="430" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0mtp7p7_di" bpmnElement="Activity_ThankYou">
<dc:Bounds x="590" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,6 +1,6 @@
from tests.base_test import BaseTest
from crc import mail
from crc.models.email import EmailModel
from tests.base_test import BaseTest
class TestEmailScript(BaseTest):
@ -9,8 +9,8 @@ class TestEmailScript(BaseTest):
workflow = self.create_workflow('email')
task_data = {
'PIComputingID': 'dhf8r',
'ApprvlApprvr1': 'lb3dp'
'PIComputingID': 'dhf8r@virginia.edu',
'ApprvlApprvr1': 'lb3dp@virginia.edu'
}
task = self.get_workflow_api(workflow).next_task

View File

@ -243,10 +243,12 @@ class TestFilesApi(BaseTest):
self.load_example_data()
spec = session.query(WorkflowSpecModel).first()
file = session.query(FileModel).filter_by(workflow_spec_id=spec.id).first()
file_id = file.id
rv = self.app.get('/v1.0/file/%i' % file.id, headers=self.logged_in_headers())
self.assert_success(rv)
rv = self.app.delete('/v1.0/file/%i' % file.id, headers=self.logged_in_headers())
rv = self.app.get('/v1.0/file/%i' % file.id, headers=self.logged_in_headers())
db.session.flush()
rv = self.app.get('/v1.0/file/%i' % file_id, headers=self.logged_in_headers())
self.assertEqual(404, rv.status_code)
def test_delete_file_after_approval(self):

View File

@ -0,0 +1,62 @@
from tests.base_test import BaseTest
from crc import session
from crc.models.study import StudyModel, StudyStatus, StudySchema
import json
class TestStudyActionsStatus(BaseTest):
def update_study_status(self, study, study_schema):
self.app.put('/v1.0/study/%i' % study.id,
content_type="application/json",
headers=self.logged_in_headers(),
data=json.dumps(study_schema))
# The error happened when the dashboard reloaded,
# in particular, when we got the studies for the user
api_response = self.app.get('/v1.0/study', headers=self.logged_in_headers(), content_type="application/json")
self.assert_success(api_response)
study_result = session.query(StudyModel).filter(StudyModel.id == study.id).first()
return study_result
def test_hold_study(self):
self.load_example_data()
study = session.query(StudyModel).first()
self.assertEqual(study.status, StudyStatus.in_progress)
study_schema = StudySchema().dump(study)
study_schema['status'] = 'hold'
study_schema['comment'] = 'This is my hold comment'
study_result = self.update_study_status(study, study_schema)
self.assertEqual(StudyStatus.hold, study_result.status)
def test_abandon_study(self):
self.load_example_data()
study = session.query(StudyModel).first()
self.assertEqual(study.status, StudyStatus.in_progress)
study_schema = StudySchema().dump(study)
study_schema['status'] = 'abandoned'
study_schema['comment'] = 'This is my abandon comment'
study_result = self.update_study_status(study, study_schema)
self.assertEqual(StudyStatus.abandoned, study_result.status)
def test_open_enrollment_study(self):
self.load_example_data()
study = session.query(StudyModel).first()
self.assertEqual(study.status, StudyStatus.in_progress)
study_schema = StudySchema().dump(study)
study_schema['status'] = 'open_for_enrollment'
study_schema['comment'] = 'This is my open enrollment comment'
study_schema['enrollment_date'] = '2021-01-04T05:00:00.000Z'
study_result = self.update_study_status(study, study_schema)
self.assertEqual(StudyStatus.open_for_enrollment, study_result.status)

View File

@ -0,0 +1,53 @@
from tests.base_test import BaseTest
from crc import mail
class TestEmailScript(BaseTest):
def test_email_script(self):
with mail.record_messages() as outbox:
workflow = self.create_workflow('email_script')
first_task = self.get_workflow_api(workflow).next_task
workflow = self.get_workflow_api(workflow)
self.complete_form(workflow, first_task, {'email_address': 'test@example.com'})
self.assertEqual(1, len(outbox))
self.assertEqual('My Email Subject', outbox[0].subject)
self.assertEqual(['test@example.com'], outbox[0].recipients)
def test_email_script_multiple(self):
with mail.record_messages() as outbox:
workflow = self.create_workflow('email_script')
first_task = self.get_workflow_api(workflow).next_task
workflow = self.get_workflow_api(workflow)
self.complete_form(workflow, first_task, {'email_address': ['test@example.com', 'test2@example.com']})
self.assertEqual(1, len(outbox))
self.assertEqual("My Email Subject", outbox[0].subject)
self.assertEqual(2, len(outbox[0].recipients))
self.assertEqual('test@example.com', outbox[0].recipients[0])
self.assertEqual('test2@example.com', outbox[0].recipients[1])
def test_bad_email_address_1(self):
workflow = self.create_workflow('email_script')
first_task = self.get_workflow_api(workflow).next_task
workflow = self.get_workflow_api(workflow)
with self.assertRaises(AssertionError):
self.complete_form(workflow, first_task, {'email_address': 'test@example'})
def test_bad_email_address_2(self):
workflow = self.create_workflow('email_script')
first_task = self.get_workflow_api(workflow).next_task
workflow = self.get_workflow_api(workflow)
with self.assertRaises(AssertionError):
self.complete_form(workflow, first_task, {'email_address': 'test'})

View File

@ -0,0 +1,41 @@
from tests.base_test import BaseTest
from crc.services.file_service import FileService
from crc.scripts.is_file_uploaded import IsFileUploaded
class TestIsFileUploaded(BaseTest):
def test_file_uploaded_pass(self):
self.load_example_data()
irb_code_1 = 'UVACompl_PRCAppr'
irb_code_2 = 'Study_App_Doc'
workflow = self.create_workflow('empty_workflow')
first_task = self.get_workflow_api(workflow).next_task
study_id = workflow.study_id
# We shouldn't have any files yet.
files = FileService.get_files_for_study(study_id)
self.assertEqual(0, len(files))
self.assertEqual(False, IsFileUploaded.do_task(IsFileUploaded, first_task, study_id, workflow.id, irb_code_1))
# Add a file
FileService.add_workflow_file(workflow_id=workflow.id,
name="something.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code_1)
# Make sure we find the file
files = FileService.get_files_for_study(study_id)
self.assertEqual(1, len(files))
self.assertEqual(True, IsFileUploaded.do_task(IsFileUploaded, first_task, study_id, workflow.id, irb_code_1))
# Add second file
FileService.add_workflow_file(workflow_id=workflow.id,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code=irb_code_2)
# Make sure we find both files.
files = FileService.get_files_for_study(study_id)
self.assertEqual(2, len(files))
self.assertEqual(True, IsFileUploaded.do_task(IsFileUploaded, first_task, study_id, workflow.id, irb_code_1))
self.assertEqual(True, IsFileUploaded.do_task(IsFileUploaded, first_task, study_id, workflow.id, irb_code_2))

View File

@ -53,7 +53,9 @@ class TestLookupService(BaseTest):
file.close()
# restart the workflow, so it can pick up the changes.
WorkflowProcessor(workflow, soft_reset=True)
processor = WorkflowProcessor.reset(workflow)
workflow = processor.workflow_model
LookupService.lookup(workflow, "sponsor", "sam", limit=10)
lookup_records = session.query(LookupFileModel).all()

View File

@ -68,7 +68,7 @@ class TestTasksApi(BaseTest):
# get the first form in the two form workflow.
workflow_api = self.get_workflow_api(workflow)
self.assertEqual('two_forms', workflow_api.workflow_spec_id)
self.assertEqual(2, len(workflow_api.navigation))
self.assertEqual(5, len(workflow_api.navigation))
self.assertIsNotNone(workflow_api.next_task.form)
self.assertEqual("UserTask", workflow_api.next_task.type)
self.assertEqual("StepOne", workflow_api.next_task.name)
@ -113,14 +113,20 @@ class TestTasksApi(BaseTest):
self.assertIsNotNone(workflow_api.navigation)
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual("Do You Have Bananas", nav[0]['title'])
self.assertEqual("Bananas?", nav[1]['title'])
self.assertEqual("FUTURE", nav[1]['state'])
self.assertEqual("yes", nav[2]['title'])
self.assertEqual("NOOP", nav[2]['state'])
self.assertEqual("no", nav[3]['title'])
self.assertEqual("NOOP", nav[3]['state'])
self.assertEqual(4, len(nav))
self.assertEqual("Do You Have Bananas", nav[1].description)
self.assertEqual("Bananas?", nav[2].description)
self.assertEqual("LIKELY", nav[2].state)
self.assertEqual("yes", nav[2].children[0].description)
self.assertEqual("LIKELY", nav[2].children[0].state)
self.assertEqual("of Bananas", nav[2].children[0].children[0].description)
self.assertEqual("EndEvent", nav[2].children[0].children[1].spec_type)
self.assertEqual("no", nav[2].children[1].description)
self.assertEqual("MAYBE", nav[2].children[1].state)
self.assertEqual("no bananas", nav[2].children[1].children[0].description)
self.assertEqual("EndEvent", nav[2].children[1].children[1].spec_type)
def test_navigation_with_exclusive_gateway(self):
workflow = self.create_workflow('exclusive_gateway_2')
@ -130,13 +136,16 @@ class TestTasksApi(BaseTest):
self.assertIsNotNone(workflow_api.navigation)
nav = workflow_api.navigation
self.assertEqual(7, len(nav))
self.assertEqual("Task 1", nav[0]['title'])
self.assertEqual("Which Branch?", nav[1]['title'])
self.assertEqual("a", nav[2]['title'])
self.assertEqual("Task 2a", nav[3]['title'])
self.assertEqual("b", nav[4]['title'])
self.assertEqual("Task 2b", nav[5]['title'])
self.assertEqual("Task 3", nav[6]['title'])
self.assertEqual("Task 1", nav[1].description)
self.assertEqual("Which Branch?", nav[2].description)
self.assertEqual("a", nav[2].children[0].description)
self.assertEqual("Task 2a", nav[2].children[0].children[0].description)
self.assertEqual("b", nav[2].children[1].description)
self.assertEqual("Task 2b", nav[2].children[1].children[0].description)
self.assertEqual(None, nav[3].description)
self.assertEqual("Task 3", nav[4].description)
self.assertEqual("EndEvent", nav[5].spec_type)
def test_document_added_to_workflow_shows_up_in_file_list(self):
self.create_reference_document()
@ -195,7 +204,7 @@ class TestTasksApi(BaseTest):
self.assertTrue(workflow_api.spec_version.startswith("v1 "))
self.assertFalse(workflow_api.is_latest_spec)
workflow_api = self.get_workflow_api(workflow, hard_reset=True)
workflow_api = self.restart_workflow_api(workflow_api, clear_data=True)
self.assertTrue(workflow_api.spec_version.startswith("v2 "))
self.assertTrue(workflow_api.is_latest_spec)
@ -204,30 +213,6 @@ class TestTasksApi(BaseTest):
self.assertTrue(workflow_api.spec_version.startswith("v2 "))
self.assertTrue(workflow_api.is_latest_spec)
def test_soft_reset_errors_out_and_next_result_is_on_original_version(self):
# Start the basic two_forms workflow and complete a task.
workflow = self.create_workflow('two_forms')
workflow_api = self.get_workflow_api(workflow)
self.complete_form(workflow, workflow_api.next_task, {"color": "blue"})
self.assertTrue(workflow_api.is_latest_spec)
# Modify the specification, with a major change that alters the flow and can't be deserialized
# effectively, if it uses the latest spec files.
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'two_forms', 'modified', 'two_forms_struc_mod.bpmn')
self.replace_file("two_forms.bpmn", file_path)
# perform a soft reset returns an error
rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' %
(workflow.id, "true", "false"),
content_type="application/json",
headers=self.logged_in_headers())
self.assert_failure(rv, error_code="unexpected_workflow_structure")
# Try again without a soft reset, and we are still ok, and on the original version.
workflow_api = self.get_workflow_api(workflow)
self.assertTrue(workflow_api.spec_version.startswith("v1 "))
self.assertFalse(workflow_api.is_latest_spec)
def test_manual_task_with_external_documentation(self):
workflow = self.create_workflow('manual_task_with_external_documentation')
@ -267,7 +252,7 @@ class TestTasksApi(BaseTest):
# get the first form in the two form workflow.
workflow = self.get_workflow_api(workflow)
navigation = self.get_workflow_api(workflow).navigation
self.assertEqual(4, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual(5, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual("UserTask", workflow.next_task.type)
self.assertEqual(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
self.assertEqual(5, workflow.next_task.multi_instance_count)
@ -385,7 +370,7 @@ class TestTasksApi(BaseTest):
navigation = workflow_api.navigation
task = workflow_api.next_task
self.assertEqual(2, len(navigation))
self.assertEqual(5, len(navigation))
self.assertEqual("UserTask", task.type)
self.assertEqual("Activity_A", task.name)
self.assertEqual("My Sub Process", task.process_name)
@ -452,8 +437,8 @@ class TestTasksApi(BaseTest):
workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow)
self.assertEqual(8, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav['state'] == "READY"]
self.assertEqual(9, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav.state == "READY"]
self.assertEqual(5, len(ready_items))
self.assertEqual("UserTask", workflow_api.next_task.type)
@ -461,8 +446,8 @@ class TestTasksApi(BaseTest):
self.assertEqual("Primary Investigator", workflow_api.next_task.title)
for i in random.sample(range(5), 5):
task = TaskSchema().load(ready_items[i]['task'])
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, task.id),
task_id = ready_items[i].task_id
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, task_id),
headers=self.logged_in_headers(),
content_type="application/json")
self.assert_success(rv)
@ -470,7 +455,7 @@ class TestTasksApi(BaseTest):
workflow = WorkflowApiSchema().load(json_data)
data = workflow.next_task.data
data['investigator']['email'] = "dhf8r@virginia.edu"
self.complete_form(workflow, task, data)
self.complete_form(workflow, workflow.next_task, data)
#tasks = self.get_workflow_api(workflow).user_tasks
workflow = self.get_workflow_api(workflow)

View File

@ -0,0 +1,28 @@
from tests.base_test import BaseTest
from crc import db
from crc.models.user import UserModel
import json
class TestUserID(BaseTest):
def test_user_id_in_request(self):
"""This assures the uid is in response via ApiError"""
workflow = self.create_workflow('failing_workflow')
user_uid = workflow.study.user_uid
user = db.session.query(UserModel).filter_by(uid=user_uid).first()
rv = self.app.get(f'/v1.0/workflow/{workflow.id}'
f'?soft_reset={str(False)}'
f'&hard_reset={str(False)}'
f'&do_engine_steps={str(True)}',
headers=self.logged_in_headers(user),
content_type="application/json")
data = json.loads(rv.data)
self.assertEqual(data['task_user'], user_uid)
def test_user_id_in_sentry(self):
"""This assures the uid is in Sentry.
We use this to send errors to Slack."""
# Currently have no clue how to do this :(
pass

View File

@ -1,6 +1,8 @@
import json
from tests.base_test import BaseTest
from crc.models.api_models import NavigationItemSchema
from crc.models.workflow import WorkflowStatus
from crc import db
from crc.api.common import ApiError
@ -62,8 +64,8 @@ class TestTasksApi(BaseTest):
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual("supervisor", nav[1]['lane'])
self.assertEqual(4, len(nav))
self.assertEqual("supervisor", nav[2].lane)
def test_get_outstanding_tasks_awaiting_current_user(self):
submitter = self.create_user(uid='lje5u')
@ -121,12 +123,10 @@ class TestTasksApi(BaseTest):
# Navigation as Submitter with ready task.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual('READY', nav[0]['state']) # First item is ready, no progress yet.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEqual('NOOP', nav[3]['state']) # Approved Path, has no operation
self.assertEqual('NOOP', nav[4]['state']) # Rejected Path, has no operation.
self.assertEqual(4, len(nav))
self.assertEqual('READY', nav[1].state) # First item is ready, no progress yet.
self.assertEqual('LOCKED', nav[2].state) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LIKELY', nav[3].state) # Third item is a gateway, which contains things that are also locked.
self.assertEqual('READY', workflow_api.next_task.state)
# Navigation as Submitter after handoff to supervisor
@ -134,10 +134,9 @@ class TestTasksApi(BaseTest):
data['supervisor'] = supervisor.uid
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEqual('COMPLETED', nav[0]['state']) # First item is ready, no progress yet.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEqual('LOCKED', workflow_api.next_task.state)
self.assertEqual('COMPLETED', nav[1].state) # First item is ready, no progress yet.
self.assertEqual('LOCKED', nav[2].state) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LIKELY', nav[3].state) # third item is a gateway, and belongs to no one
# In the event the next task is locked, we should say something sensible here.
# It is possible to look at the role of the task, and say The next task "TASK TITLE" will
# be handled by 'dhf8r', who is full-filling the role of supervisor. the Task Data
@ -149,10 +148,9 @@ class TestTasksApi(BaseTest):
# Navigation as Supervisor
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEqual('READY', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEqual('LOCKED', nav[1].state) # First item belongs to the submitter, and is locked.
self.assertEqual('READY', nav[2].state) # Second item is ready, as we are now the supervisor.
self.assertEqual('LIKELY', nav[3].state) # Feedback is locked.
self.assertEqual('READY', workflow_api.next_task.state)
data = workflow_api.next_task.data
@ -161,28 +159,33 @@ class TestTasksApi(BaseTest):
# Navigation as Supervisor, after completing task.
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEqual('COMPLETED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('COMPLETED', nav[2]['state']) # third item is a gateway, and is now complete.
self.assertEqual('LOCKED', nav[1].state) # First item belongs to the submitter, and is locked.
self.assertEqual('COMPLETED', nav[2].state) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('READY', nav[3].state) # Gateway is ready, and should be unfolded
self.assertEqual(None, nav[3].children[0].state) # sequence flow for approved is none - we aren't going this way.
self.assertEqual('READY', nav[3].children[1].state) # sequence flow for denied is ready
self.assertEqual('LOCKED', nav[3].children[1].children[0].state) # Feedback is locked, it belongs to submitter
self.assertEqual('LOCKED', nav[3].children[1].children[0].state) # Approval is locked, it belongs to the submitter
self.assertEqual('LOCKED', workflow_api.next_task.state)
# Navigation as Submitter, coming back in to a rejected workflow to view the rejection message.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual('COMPLETED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEqual('READY', workflow_api.next_task.state)
self.assertEqual(4, len(nav))
self.assertEqual('COMPLETED', nav[1].state) # First item belongs to the submitter, and is locked.
self.assertEqual('LOCKED', nav[2].state) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('READY', nav[3].state)
self.assertEqual(None, nav[3].children[0].state) # sequence flow for approved is none - we aren't going this way.
self.assertEqual('READY', nav[3].children[1].state) # sequence flow for denied is ready
self.assertEqual('READY', nav[3].children[1].children[0].state) # Feedback is locked, it belongs to submitter
self.assertEqual('READY', nav[3].children[1].children[0].state) # Approval is locked, it belongs to the submitter
# Navigation as Submitter, re-completing the original request a second time, and sending it for review.
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual('READY', nav[0]['state']) # When you loop back the task is again in the ready state.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEqual('READY', nav[1].state) # When you loop back the task is again in the ready state.
self.assertEqual('LOCKED', nav[2].state) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('COMPLETED', nav[3].state) # Feedback is completed
self.assertEqual('READY', workflow_api.next_task.state)
data["favorite_color"] = "blue"
@ -225,7 +228,8 @@ class TestTasksApi(BaseTest):
self.assertEqual(1, len(self.get_assignment_task_events(supervisor.uid)))
# Resetting the workflow at this point should clear the event log.
workflow_api = self.get_workflow_api(workflow, hard_reset=True, user_uid=submitter.uid)
workflow_api = self.restart_workflow_api(workflow, user_uid=submitter.uid)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
self.assertEqual(0, len(self.get_assignment_task_events(supervisor.uid)))
# Re-complete first task, and awaiting tasks should shift to 0 for for submitter, and 1 for supervisor

View File

@ -0,0 +1,18 @@
from tests.base_test import BaseTest
from crc.services.workflow_service import WorkflowService
from crc.api.common import ApiError
from jinja2.exceptions import TemplateSyntaxError
class TestValidateEndEvent(BaseTest):
def test_validate_end_event(self):
error_string = """Error processing template for task EndEvent_1qvyxg7: expected token 'end of statement block', got '='"""
self.load_example_data()
spec_model = self.load_test_spec('verify_end_event')
try:
WorkflowService.test_spec(spec_model.id)
except ApiError as e:
self.assertEqual(str(e), error_string)

View File

@ -1,7 +1,7 @@
from unittest.mock import patch
from tests.base_test import BaseTest
from crc import db
from tests.base_test import BaseTest
from crc.api.workflow_sync import get_all_spec_state, \
get_changed_workflows, \
get_workflow_spec_files, \

View File

@ -0,0 +1,32 @@
from tests.base_test import BaseTest
from crc import session
from crc.api.common import ApiError
from crc.models.workflow import WorkflowSpecModel
from crc.services.file_service import FileService
class TestDuplicateWorkflowSpecFile(BaseTest):
def test_duplicate_workflow_spec_file(self):
# We want this to fail.
# Users should not be able to upload a file that already exists.
self.load_example_data()
spec = session.query(WorkflowSpecModel).first()
# Add a file
file_model = FileService.add_workflow_spec_file(spec,
name="something.png",
content_type="text",
binary_data=b'1234')
self.assertEqual(file_model.name, 'something.png')
self.assertEqual(file_model.content_type, 'text')
# Try to add it again
try:
FileService.add_workflow_spec_file(spec,
name="something.png",
content_type="text",
binary_data=b'5678')
except ApiError as ae:
self.assertEqual(ae.message, 'If you want to replace the file, use the update mechanism.')

View File

@ -0,0 +1,14 @@
import json
from tests.base_test import BaseTest
class TestFormFieldName(BaseTest):
def test_form_field_name(self):
spec_model = self.load_test_spec('workflow_form_field_name')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
json_data = json.loads(rv.get_data(as_text=True))
self.assertEqual(json_data[0]['message'],
'When populating all fields ... Invalid Field name: "user-title". A field ID must begin '
'with a letter, and can only contain letters, numbers, and "_"')

View File

@ -0,0 +1,14 @@
import json
from tests.base_test import BaseTest
class TestFormFieldType(BaseTest):
def test_form_field_type(self):
spec_model = self.load_test_spec('workflow_form_field_type')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
json_data = json.loads(rv.get_data(as_text=True))
self.assertEqual(json_data[0]['message'],
'When populating all fields ... Type is missing for field "name". A field type must be provided.')
# print('TestFormFieldType: Good Form')

View File

@ -173,28 +173,6 @@ class TestWorkflowProcessor(BaseTest):
self.assertEqual("workflow_validation_error", context.exception.code)
self.assertTrue("bpmn:startEvent" in context.exception.message)
def test_workflow_spec_key_error(self):
"""Frequently seeing errors in the logs about a 'Key' error, where a workflow
references something that doesn't exist in the midst of processing. Want to
make sure we produce errors to the front end that allows us to debug this."""
# Start the two_forms workflow, and enter some data in the first form.
self.load_example_data()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("two_forms")
processor = self.get_processor(study, workflow_spec_model)
self.assertEqual(processor.workflow_model.workflow_spec_id, workflow_spec_model.id)
task = processor.next_task()
task.data = {"color": "blue"}
processor.complete_task(task)
# Modify the specification, with a major change.
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'two_forms', 'modified', 'two_forms_struc_mod.bpmn')
self.replace_file("two_forms.bpmn", file_path)
# Attempting a soft update on a structural change should raise a sensible error.
with self.assertRaises(ApiError) as context:
processor3 = WorkflowProcessor(processor.workflow_model, soft_reset=True)
self.assertEqual("unexpected_workflow_structure", context.exception.code)
def test_workflow_with_bad_expression_raises_sensible_error(self):
self.load_example_data()
@ -301,7 +279,9 @@ class TestWorkflowProcessor(BaseTest):
self.assertFalse(processor2.is_latest_spec) # Still at version 1.
# Do a hard reset, which should bring us back to the beginning, but retain the data.
processor3 = WorkflowProcessor(processor.workflow_model, hard_reset=True)
WorkflowProcessor.reset(processor2.workflow_model)
processor3 = WorkflowProcessor(processor.workflow_model)
processor3.do_engine_steps()
self.assertEqual("Step 1", processor3.next_task().task_spec.description)
self.assertTrue(processor3.is_latest_spec) # Now at version 2.
task = processor3.next_task()

View File

@ -52,11 +52,11 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task_list = processor.get_ready_user_tasks()
processor.complete_task(task_list[0])
processor.do_engine_steps()
nav_list = processor.bpmn_workflow.get_nav_list()
nav_list = processor.bpmn_workflow.get_flat_nav_list()
processor.save()
# reload after save
processor = WorkflowProcessor(workflow_spec_model)
nav_list2 = processor.bpmn_workflow.get_nav_list()
nav_list2 = processor.bpmn_workflow.get_flat_nav_list()
self.assertEqual(nav_list,nav_list2)
@patch('crc.services.study_service.StudyService.get_investigators')
@ -158,7 +158,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
self.assertEqual(3, len(next_user_tasks))
# There should be six tasks in the navigation: start event, the script task, end event, and three tasks
# for the three executions of hte multi-instance.
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(7, len(processor.bpmn_workflow.get_flat_nav_list()))
# We can complete the tasks out of order.
task = next_user_tasks[2]
@ -171,12 +171,12 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
# Assure navigation picks up the label of the current element variable.
nav = WorkflowService.processor_to_workflow_api(processor, task).navigation
self.assertEqual("Primary Investigator", nav[2].title)
self.assertEqual("Primary Investigator", nav[2].description)
task.update_data({"investigator": {"email": "dhf8r@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(7, len(processor.bpmn_workflow.get_flat_nav_list()))
task = next_user_tasks[0]
api_task = WorkflowService.spiff_task_to_api_task(task)
@ -184,7 +184,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task.update_data({"investigator":{"email":"asd3v@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(7, len(processor.bpmn_workflow.get_flat_nav_list()))
task = next_user_tasks[1]
api_task = WorkflowService.spiff_task_to_api_task(task)
@ -192,7 +192,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task.update_data({"investigator":{"email":"asdf32@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(7, len(processor.bpmn_workflow.get_flat_nav_list()))
# Completing the tasks out of order, still provides the correct information.
expected = self.mock_investigator_response
@ -203,4 +203,4 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task.data['StudyInfo']['investigators'])
self.assertEqual(WorkflowStatus.complete, processor.get_status())
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(7, len(processor.bpmn_workflow.get_flat_nav_list()))

View File

@ -0,0 +1,34 @@
from tests.base_test import BaseTest
class TestMessageEvent(BaseTest):
def test_message_event(self):
workflow = self.create_workflow('message_event')
first_task = self.get_workflow_api(workflow).next_task
self.assertEqual('Activity_GetData', first_task.name)
workflow_api = self.get_workflow_api(workflow)
result = self.complete_form(workflow_api, first_task, {'formdata': 'asdf'})
self.assertIn('formdata', result.next_task.data)
self.assertEqual('asdf', result.next_task.data['formdata'])
workflow_api = self.get_workflow_api(workflow)
self.assertEqual('Activity_HowMany', self.get_workflow_api(workflow_api).next_task.name)
# restart with data. should land at beginning with data
workflow_api = self.restart_workflow_api(result)
first_task = self.get_workflow_api(workflow_api).next_task
self.assertEqual('Activity_GetData', first_task.name)
self.assertIn('formdata', workflow_api.next_task.data)
self.assertEqual('asdf', workflow_api.next_task.data['formdata'])
# restart without data.
workflow_api = self.restart_workflow_api(workflow_api, clear_data=True)
first_task = self.get_workflow_api(workflow).next_task
self.assertEqual('Activity_GetData', first_task.name)
self.assertNotIn('formdata', workflow_api.next_task.data)
print('Nice Test')