Merge branch 'dev' into feature/delete-variable-script-584

# Conflicts:
#	Pipfile.lock
This commit is contained in:
mike cullerton 2022-02-25 15:54:34 -05:00
commit f2c97f80d5
27 changed files with 861 additions and 349 deletions

View File

@ -12,7 +12,7 @@ coverage = "*"
alembic = "*" alembic = "*"
coverage = "*" coverage = "*"
docxtpl = "*" docxtpl = "*"
flask = "<2" flask = "*"
celery = "<5" celery = "<5"
flask-admin = "*" flask-admin = "*"
flask-bcrypt = "*" flask-bcrypt = "*"

519
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "bf333e0e7aaaff0988808dab8191f9711f1100f3af3dc35123672086c290667c" "sha256": "eb924ba10433552dcde585064a905bb8b78e96b28fc23391eee486ca34e22aad"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -48,11 +48,11 @@
}, },
"apscheduler": { "apscheduler": {
"hashes": [ "hashes": [
"sha256:5cf344ebcfbdaa48ae178c029c055cec7bc7a4a47c21e315e4d1f08bd35f2355", "sha256:236dbf7244200ffc79c6c0b9ff7d2ed10e7e985f37f48d4a23f4142f4967dcb5",
"sha256:c22cb14b411a31435eb2c530dfbbec948ac63015b517087c7978adb61b574865" "sha256:f8a3e6e4d178de40fdbd5a3eefcc217588fc7f42f443e2335bb2dc29daf99357"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.8.1" "version": "==3.9.0.post1"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
@ -209,11 +209,11 @@
}, },
"click": { "click": {
"hashes": [ "hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '3.6'",
"version": "==7.1.2" "version": "==8.0.4"
}, },
"clickclick": { "clickclick": {
"hashes": [ "hashes": [
@ -242,58 +242,58 @@
"swagger-ui" "swagger-ui"
], ],
"hashes": [ "hashes": [
"sha256:66620b10b2c03eab6af981f8489d0ff7ada19f66710274effc71258fb8221419", "sha256:2d163976fbc8766038d71f4232ace07826be7dd0a8fe7a539ce5fc8a80f1ab35",
"sha256:b5e5ba236894a02b8da4d10face412f471abb6ff77de10dad32fa88cb894acf7" "sha256:c08b530418a1c448d34cd142713ddd1b245e7694f45cd80533fe8538b64aa7eb"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.11.1" "version": "==2.12.0"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
"sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0", "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d",
"sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554", "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf",
"sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb", "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7",
"sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2", "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6",
"sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b", "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4",
"sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8", "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059",
"sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba", "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39",
"sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734", "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536",
"sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2", "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac",
"sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f", "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c",
"sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0", "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903",
"sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1", "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d",
"sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd", "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05",
"sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687", "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684",
"sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1", "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1",
"sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c", "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f",
"sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa", "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7",
"sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8", "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca",
"sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38", "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad",
"sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8", "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca",
"sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167", "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d",
"sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27", "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92",
"sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145", "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4",
"sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa", "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf",
"sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a", "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6",
"sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed", "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1",
"sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793", "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4",
"sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4", "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359",
"sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217", "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3",
"sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e", "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620",
"sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6", "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512",
"sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d", "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69",
"sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320", "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2",
"sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f", "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518",
"sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce", "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0",
"sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975", "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa",
"sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10", "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4",
"sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525", "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e",
"sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda", "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1",
"sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1" "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.3.1" "version": "==6.3.2"
}, },
"dateparser": { "dateparser": {
"hashes": [ "hashes": [
@ -343,11 +343,11 @@
}, },
"flask": { "flask": {
"hashes": [ "hashes": [
"sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196", "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f",
"sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22" "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.1.4" "version": "==2.0.3"
}, },
"flask-admin": { "flask-admin": {
"hashes": [ "hashes": [
@ -427,11 +427,11 @@
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:26ac35c212d1f7b16036361ca5cff3ec66e11753a0d677fb6c48fa4e1a9dd8d6", "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704",
"sha256:fc8868f63a2e6d268fb25f481995ba185a85a66fcad126f039323ff6635669ee" "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1.26" "version": "==3.1.27"
}, },
"greenlet": { "greenlet": {
"hashes": [ "hashes": [
@ -549,35 +549,29 @@
"markers": "python_version >= '3.5'", "markers": "python_version >= '3.5'",
"version": "==0.5.1" "version": "==0.5.1"
}, },
"isodate": {
"hashes": [
"sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96",
"sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"
],
"version": "==0.6.1"
},
"itsdangerous": { "itsdangerous": {
"hashes": [ "hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" "sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '3.7'",
"version": "==1.1.0" "version": "==2.1.0"
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8",
"sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '3.6'",
"version": "==2.11.3" "version": "==3.0.3"
}, },
"jsonschema": { "jsonschema": {
"hashes": [ "hashes": [
"sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83",
"sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" "sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823"
], ],
"version": "==3.2.0" "markers": "python_version >= '3.7'",
"version": "==4.4.0"
}, },
"kombu": { "kombu": {
"hashes": [ "hashes": [
@ -600,69 +594,70 @@
}, },
"lxml": { "lxml": {
"hashes": [ "hashes": [
"sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4", "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169",
"sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f", "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428",
"sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a", "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc",
"sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944", "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85",
"sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1", "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696",
"sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d", "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507",
"sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d", "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3",
"sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e", "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430",
"sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d", "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03",
"sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a", "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9",
"sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675", "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b",
"sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3", "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7",
"sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55", "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5",
"sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60", "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654",
"sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d", "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca",
"sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6", "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9",
"sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e", "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c",
"sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5", "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63",
"sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5", "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe",
"sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42", "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9",
"sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0", "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9",
"sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d", "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1",
"sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489", "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939",
"sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440", "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68",
"sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e", "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613",
"sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6", "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63",
"sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e", "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e",
"sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f", "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4",
"sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d", "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79",
"sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03", "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1",
"sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9", "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e",
"sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9", "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141",
"sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd", "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb",
"sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6", "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939",
"sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4", "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a",
"sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868", "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93",
"sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267", "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9",
"sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2", "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2",
"sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4", "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6",
"sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24", "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa",
"sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2", "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150",
"sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db", "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea",
"sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a", "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33",
"sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8", "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76",
"sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175", "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807",
"sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851", "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a",
"sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b", "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4",
"sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e", "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15",
"sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986", "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f",
"sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f", "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429",
"sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419", "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c",
"sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7", "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5",
"sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7", "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870",
"sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36", "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b",
"sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc", "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8",
"sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b", "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c",
"sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e", "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87",
"sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17", "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0",
"sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3", "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23",
"sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77" "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170",
"sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.7.1" "version": "==4.8.0"
}, },
"mako": { "mako": {
"hashes": [ "hashes": [
@ -682,78 +677,49 @@
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
"sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3",
"sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8",
"sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759",
"sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed",
"sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989",
"sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3",
"sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a",
"sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c",
"sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c",
"sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8",
"sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454",
"sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad",
"sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d",
"sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635",
"sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61",
"sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea",
"sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49",
"sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce",
"sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e",
"sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f",
"sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f",
"sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f",
"sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7",
"sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a",
"sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7",
"sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076",
"sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb",
"sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7",
"sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7",
"sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c",
"sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26",
"sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c",
"sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8",
"sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448",
"sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956",
"sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05",
"sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1",
"sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357",
"sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea",
"sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"
"sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6",
"sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
"sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e",
"sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f",
"sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
"sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145",
"sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
"sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
"sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a",
"sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207",
"sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
"sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd",
"sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
"sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
"sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9",
"sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.7'",
"version": "==2.0.1" "version": "==2.1.0"
}, },
"marshmallow": { "marshmallow": {
"hashes": [ "hashes": [
@ -804,23 +770,6 @@
"markers": "python_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64'", "markers": "python_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64'",
"version": "==1.22.2" "version": "==1.22.2"
}, },
"openapi-schema-validator": {
"hashes": [
"sha256:230db361c71a5b08b25ec926797ac8b59a8f499bbd7316bd15b6cd0fc9aea5df",
"sha256:8ef097b78c191c89d9a12cdf3d311b2ecf9d3b80bbe8610dbc67a812205a6a8d",
"sha256:af023ae0d16372cf8dd0d128c9f3eaa080dc3cd5dfc69e6a247579f25bd10503"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.1.6"
},
"openapi-spec-validator": {
"hashes": [
"sha256:43d606c5910ed66e1641807993bd0a981de2fc5da44f03e1c4ca2bb65b94b68e",
"sha256:49d7da81996714445116f6105c9c5955c0e197ef8636da4f368c913f64753443"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.3.3"
},
"openpyxl": { "openpyxl": {
"hashes": [ "hashes": [
"sha256:40f568b9829bf9e446acfffce30250ac1fa39035124d55fc024025c41481c90f", "sha256:40f568b9829bf9e446acfffce30250ac1fa39035124d55fc024025c41481c90f",
@ -1002,10 +951,30 @@
}, },
"pyrsistent": { "pyrsistent": {
"hashes": [ "hashes": [
"sha256:aa2ae1c2e496f4d6777f869ea5de7166a8ccb9c2e06ebcf6c7ff1b670c98c5ef" "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c",
"sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc",
"sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e",
"sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26",
"sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec",
"sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286",
"sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045",
"sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec",
"sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8",
"sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c",
"sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca",
"sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22",
"sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a",
"sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96",
"sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc",
"sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1",
"sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07",
"sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6",
"sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b",
"sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5",
"sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"
], ],
"markers": "python_version >= '2.7'", "markers": "python_version >= '3.7'",
"version": "==0.16.1" "version": "==0.18.1"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
@ -1266,7 +1235,7 @@
}, },
"spiffworkflow": { "spiffworkflow": {
"git": "https://github.com/sartography/SpiffWorkflow", "git": "https://github.com/sartography/SpiffWorkflow",
"ref": "5c9301a3f47bd3dbeae72ffefd9d25cfee35fd0e" "ref": "3904fdbc90f7fabddfe1e6a4f802963a2364ae7e"
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
@ -1376,11 +1345,11 @@
}, },
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.0.1" "version": "==2.0.3"
}, },
"wrapt": { "wrapt": {
"hashes": [ "hashes": [
@ -1483,50 +1452,50 @@
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c", "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
"sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0", "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d",
"sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554", "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf",
"sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb", "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7",
"sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2", "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6",
"sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b", "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4",
"sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8", "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059",
"sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba", "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39",
"sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734", "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536",
"sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2", "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac",
"sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f", "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c",
"sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0", "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903",
"sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1", "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d",
"sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd", "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05",
"sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687", "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684",
"sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1", "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1",
"sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c", "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f",
"sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa", "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7",
"sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8", "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca",
"sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38", "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad",
"sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8", "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca",
"sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167", "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d",
"sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27", "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92",
"sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145", "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4",
"sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa", "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf",
"sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a", "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6",
"sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed", "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1",
"sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793", "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4",
"sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4", "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359",
"sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217", "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3",
"sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e", "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620",
"sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6", "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512",
"sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d", "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69",
"sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320", "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2",
"sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f", "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518",
"sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce", "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0",
"sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975", "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa",
"sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10", "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4",
"sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525", "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e",
"sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda", "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1",
"sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1" "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.3.1" "version": "==6.3.2"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [

View File

@ -86,7 +86,7 @@ GIT_BRANCH = environ.get('GIT_BRANCH', None) # example: 'main'
GIT_MERGE_BRANCH = environ.get('GIT_MERGE_BRANCH', None) # Example: 'staging' GIT_MERGE_BRANCH = environ.get('GIT_MERGE_BRANCH', None) # Example: 'staging'
# Email configuration # Email configuration
DEFAULT_SENDER = 'uvacrconnect@virginia.edu' DEFAULT_SENDER = 'crconnect2@virginia.edu'
FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com'] FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com']
MAIL_DEBUG = environ.get('MAIL_DEBUG', default=True) MAIL_DEBUG = environ.get('MAIL_DEBUG', default=True)
MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io') MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io')

View File

@ -1,6 +1,7 @@
import time import time
import uuid import uuid
from SpiffWorkflow.util.metrics import firsttime, timeit, sincetime
from flask import g from flask import g
from crc import session from crc import session
@ -209,7 +210,6 @@ def get_workflow(workflow_id, do_engine_steps=True):
workflow_api_model = WorkflowService.processor_to_workflow_api(processor) workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
return WorkflowApiSchema().dump(workflow_api_model) return WorkflowApiSchema().dump(workflow_api_model)
def restart_workflow(workflow_id, clear_data=False, delete_files=False): def restart_workflow(workflow_id, clear_data=False, delete_files=False):
"""Restart a workflow with the latest spec. """Restart a workflow with the latest spec.
Clear data allows user to restart the workflow without previous data.""" Clear data allows user to restart the workflow without previous data."""
@ -219,7 +219,8 @@ def restart_workflow(workflow_id, clear_data=False, delete_files=False):
processor.save() processor.save()
WorkflowService.update_task_assignments(processor) WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor) workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
return WorkflowApiSchema().dump(workflow_api_model) api_model = WorkflowApiSchema().dump(workflow_api_model)
return api_model
def get_task_events(action = None, workflow = None, study = None): def get_task_events(action = None, workflow = None, study = None):

View File

@ -3,6 +3,7 @@ import enum
import marshmallow import marshmallow
from marshmallow import EXCLUDE, post_load, fields, INCLUDE from marshmallow import EXCLUDE, post_load, fields, INCLUDE
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import deferred
from crc import db, ma from crc import db, ma
@ -104,10 +105,10 @@ class WorkflowStatus(enum.Enum):
class WorkflowModel(db.Model): class WorkflowModel(db.Model):
__tablename__ = 'workflow' __tablename__ = 'workflow'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
bpmn_workflow_json = db.Column(db.JSON) bpmn_workflow_json = deferred(db.Column(db.JSON))
status = db.Column(db.Enum(WorkflowStatus)) status = db.Column(db.Enum(WorkflowStatus))
study_id = db.Column(db.Integer, db.ForeignKey('study.id')) study_id = db.Column(db.Integer, db.ForeignKey('study.id'))
study = db.relationship("StudyModel", backref='workflow') study = db.relationship("StudyModel", backref='workflow', lazy='select')
workflow_spec_id = db.Column(db.String) workflow_spec_id = db.Column(db.String)
total_tasks = db.Column(db.Integer, default=0) total_tasks = db.Column(db.Integer, default=0)
completed_tasks = db.Column(db.Integer, default=0) completed_tasks = db.Column(db.Integer, default=0)

View File

@ -1,3 +1,5 @@
from SpiffWorkflow.util.metrics import timeit
from crc.scripts.script import Script from crc.scripts.script import Script
from crc.services.user_file_service import UserFileService from crc.services.user_file_service import UserFileService
@ -14,10 +16,6 @@ class IsFileUploaded(Script):
def do_task(self, task, study_id, workflow_id, *args, **kwargs): def do_task(self, task, study_id, workflow_id, *args, **kwargs):
files = UserFileService.get_files_for_study(study_id) doc_code = args[0]
if len(files) > 0: files = UserFileService.get_files_for_study(study_id, irb_doc_code=doc_code)
doc_code = args[0] return len(files) > 0
for file in files:
if doc_code == file.irb_doc_code:
return True
return False

View File

@ -0,0 +1,66 @@
from crc import session
from crc.api.common import ApiError
from crc.models.file import FileModel, FileDataModel
from crc.scripts.script import Script
from io import BytesIO
from openpyxl import load_workbook
from openpyxl.writer.excel import save_virtual_workbook
class ModifySpreadsheet(Script):
@staticmethod
def get_parameters(args, kwargs):
parameters = {}
if len(args) == 3 or ('irb_doc_code' in kwargs and 'cell' in kwargs and 'text' in kwargs):
if 'irb_doc_code' in kwargs and 'cell' in kwargs and 'text' in kwargs:
parameters['irb_doc_code'] = (kwargs['irb_doc_code'])
parameters['cell'] = (kwargs['cell'])
parameters['text'] = (kwargs['text'])
else:
parameters['irb_doc_code'] = (args[0])
parameters['cell'] = (args[1])
parameters['text'] = (args[2])
return parameters
def get_description(self):
return """Script to modify an existing spreadsheet.
It inserts text into a spreadsheet in the cell indicated.
Requires 'irb_doc_code', 'cell', and 'text' parameters.
Example: modify_spreadsheet('Finance_BCA', 'C4', 'This is my inserted text')
Example: modify_spreadsheet(irb_doc_code='Finance_BCA', cell='C4', text='This is my inserted text')
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
parameters = self.get_parameters(args, kwargs)
if len(parameters) == 3:
return True
else:
raise ApiError(code='missing_parameters',
message='The modify_spreadsheet script requires 3 parameters: irb_doc_code, cell, and text')
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
parameters = self.get_parameters(args, kwargs)
if len(parameters) == 3:
spreadsheet = session.query(FileModel). \
filter(FileModel.workflow_id == workflow_id). \
filter(FileModel.irb_doc_code == parameters['irb_doc_code']).\
first()
if spreadsheet:
spreadsheet_data = session.query(FileDataModel).\
filter(FileDataModel.file_model_id==spreadsheet.id).\
first()
workbook = load_workbook(BytesIO(spreadsheet_data.data))
sheet = workbook.active
sheet[parameters['cell']] = parameters['text']
data_string = save_virtual_workbook(workbook)
spreadsheet_data.data = data_string
session.commit()
else:
raise ApiError(code='missing_spreadsheet',
message=f"The spreadshhet you want to modify does not exist. Workflow ID is {workflow_id}, and IRB Doc Code is {parameters['irb_doc_code']}")
else:
raise ApiError(code='missing_parameters',
message='The modify_spreadsheet script requires 3 parameters: irb_doc_code, cell, and text')

View File

@ -2,14 +2,20 @@ import importlib
import os import os
import pkgutil import pkgutil
from SpiffWorkflow.util.metrics import timeit
from crc.api.common import ApiError from crc.api.common import ApiError
# Generally speaking, having some global in a flask app is TERRIBLE.
# This is here, because after loading the application this will never change under
# any known condition, and it is expensive to calculate it everytime.
SCRIPT_SUB_CLASSES = None
class Script(object): class Script(object):
""" Provides an abstract class that defines how scripts should work, this """ Provides an abstract class that defines how scripts should work, this
must be extended in all Script Tasks.""" must be extended in all Script Tasks."""
SUB_CLASSES = []
def get_description(self): def get_description(self):
raise ApiError("invalid_script", raise ApiError("invalid_script",
@ -82,10 +88,11 @@ class Script(object):
@classmethod @classmethod
def get_all_subclasses(cls): def get_all_subclasses(cls):
# This is expensive to generate, so reuse it if possible. # This is expensive to generate, never changes after we load up.
if not cls.SUB_CLASSES: global SCRIPT_SUB_CLASSES
cls.SUB_CLASSES = Script._get_all_subclasses(Script) if not SCRIPT_SUB_CLASSES:
return cls.SUB_CLASSES SCRIPT_SUB_CLASSES = Script._get_all_subclasses(Script)
return SCRIPT_SUB_CLASSES
@staticmethod @staticmethod
def _get_all_subclasses(cls): def _get_all_subclasses(cls):

View File

@ -0,0 +1,75 @@
from crc import session
from crc.api.common import ApiError
from crc.models.api_models import WorkflowApi, WorkflowApiSchema
from crc.models.workflow import WorkflowModel, WorkflowStatus
from crc.scripts.script import Script
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
class StartWorkflow(Script):
@staticmethod
def get_workflow(workflow_id):
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model)
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 get_description(self):
return """Script to start a workflow programmatically.
It requires a workflow_spec_id.
It accepts the workflow_spec_id as a positional argument
or with the keyword 'workflow_spec_id'"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
if len(args) == 1 or 'workflow_spec_id' in kwargs:
if 'workflow_spec_id' in kwargs:
workflow_spec_id = kwargs['workflow_spec_id']
else:
workflow_spec_id = args[0]
workflow_api = WorkflowApi(1234,
WorkflowStatus('user_input_required'),
'next_task',
'navigation',
workflow_spec_id,
'total_tasks',
'completed_tasks',
'last_updated',
'is_review',
'title',
study_id)
return WorkflowApiSchema().dump(workflow_api)
else:
raise ApiError(code='missing_parameter',
message=f'The start_workflow script requires a workflow id')
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
if len(args) == 1 or 'workflow_spec_id' in kwargs:
if 'workflow_spec_id' in kwargs:
workflow_spec_id = kwargs['workflow_spec_id']
else:
workflow_spec_id = args[0]
workflow = session.query(WorkflowModel).\
filter(WorkflowModel.study_id==study_id).\
filter(WorkflowModel.workflow_spec_id==workflow_spec_id).\
first()
if workflow:
workflow_api = self.get_workflow(workflow.id)
return workflow_api
else:
raise ApiError(code='unknown_workflow',
message=f"We could not find a workflow with workflow_spec_id '{workflow_spec_id}'.")
else:
raise ApiError(code='missing_parameter',
message=f'The start_workflow script requires a workflow id')

View File

@ -166,7 +166,6 @@ Please note this is just a few examples, ALL known document types are returned i
# in order to test multiple paths thru the workflow # in order to test multiple paths thru the workflow
return self.do_task(task, study_id, workflow_id, *args, **kwargs) return self.do_task(task, study_id, workflow_id, *args, **kwargs)
@timeit
def do_task(self, task, study_id, workflow_id, *args, **kwargs): def do_task(self, task, study_id, workflow_id, *args, **kwargs):
self.check_args(args, 2) self.check_args(args, 2)
prefix = None prefix = None

View File

@ -3,6 +3,7 @@ from json import JSONDecodeError
from typing import List, Optional from typing import List, Optional
import requests import requests
from SpiffWorkflow.util.metrics import timeit
from crc import app from crc import app
from crc.api.common import ApiError from crc.api.common import ApiError

View File

@ -113,6 +113,13 @@ class SpecFileService(FileSystemService):
except etree.XMLSyntaxError as xse: except etree.XMLSyntaxError as xse:
raise ApiError("invalid_xml", "Failed to parse xml: " + str(xse), file_name=file_name) raise ApiError("invalid_xml", "Failed to parse xml: " + str(xse), file_name=file_name)
except ValidationException as ve:
if ve.args[0].find('No executable process tag found') >= 0:
raise ApiError(code='missing_executable_option',
message='No executable process tag found. Please make sure the Executable option is set in the workflow.')
else:
raise ApiError(code='validation_error',
message=f'There was an error validating your workflow. Original message is: {ve}')
else: else:
raise ApiError("invalid_xml", "Only a BPMN can be the primary file.", file_name=file_name) raise ApiError("invalid_xml", "Only a BPMN can be the primary file.", file_name=file_name)

View File

@ -6,6 +6,8 @@ import requests
from SpiffWorkflow import WorkflowException from SpiffWorkflow import WorkflowException
from SpiffWorkflow.bpmn.PythonScriptEngine import Box from SpiffWorkflow.bpmn.PythonScriptEngine import Box
from SpiffWorkflow.exceptions import WorkflowTaskExecException from SpiffWorkflow.exceptions import WorkflowTaskExecException
from SpiffWorkflow.util.metrics import timeit, firsttime, sincetime, LOG
from flask import g
from ldap3.core.exceptions import LDAPSocketOpenError from ldap3.core.exceptions import LDAPSocketOpenError
from crc import db, session, app from crc import db, session, app
@ -251,8 +253,20 @@ class StudyService(object):
session.delete(workflow) session.delete(workflow)
session.commit() session.commit()
@classmethod
def get_documents_status(cls, study_id, force=False):
"""Returns a list of documents related to the study, and any file information
that is available. This is a fairly expensive operation. So we cache the results
in Flask's g. Each fresh api request will get an up to date list, but we won't
re-create it sevearl times."""
if 'doc_statuses' not in g:
g.doc_statuses = {}
if study_id not in g.doc_statuses or force:
g.doc_statuses[study_id] = StudyService.__get_documents_status(study_id)
return g.doc_statuses[study_id]
@staticmethod @staticmethod
def get_documents_status(study_id): def __get_documents_status(study_id):
"""Returns a list of documents related to the study, and any file information """Returns a list of documents related to the study, and any file information
that is available..""" that is available.."""
@ -265,12 +279,13 @@ class StudyService(object):
pb_docs = [] pb_docs = []
else: else:
pb_docs = [] pb_docs = []
# Loop through all known document types, get the counts for those files, # Loop through all known document types, get the counts for those files,
# and use pb_docs to mark those as required. # and use pb_docs to mark those as required.
doc_dictionary = DocumentService.get_dictionary() doc_dictionary = DocumentService.get_dictionary()
documents = {} documents = {}
study_files = UserFileService.get_files_for_study(study_id=study_id)
for code, doc in doc_dictionary.items(): for code, doc in doc_dictionary.items():
doc['required'] = False doc['required'] = False
@ -284,6 +299,7 @@ class StudyService(object):
doc['study_id'] = study_id doc['study_id'] = study_id
doc['code'] = code doc['code'] = code
# Make a display name out of categories # Make a display name out of categories
name_list = [] name_list = []
for cat_key in ['category1', 'category2', 'category3']: for cat_key in ['category1', 'category2', 'category3']:
@ -291,11 +307,14 @@ class StudyService(object):
name_list.append(doc[cat_key]) name_list.append(doc[cat_key])
doc['display_name'] = ' / '.join(name_list) doc['display_name'] = ' / '.join(name_list)
# For each file, get associated workflow status # For each file, get associated workflow status
doc_files = UserFileService.get_files_for_study(study_id=study_id, irb_doc_code=code) doc_files = list(filter(lambda f: f.irb_doc_code == code, study_files))
# doc_files = UserFileService.get_files_for_study(study_id=study_id, irb_doc_code=code)
doc['count'] = len(doc_files) doc['count'] = len(doc_files)
doc['files'] = [] doc['files'] = []
for file_model in doc_files: for file_model in doc_files:
file = File.from_models(file_model, UserFileService.get_file_data(file_model.id), []) file = File.from_models(file_model, UserFileService.get_file_data(file_model.id), [])
file_data = FileSchema().dump(file) file_data = FileSchema().dump(file)

View File

@ -1,3 +1,4 @@
import json
from typing import List from typing import List
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
@ -59,7 +60,6 @@ class CustomBpmnScriptEngine(PythonScriptEngine):
"Error evaluating expression " "Error evaluating expression "
"'%s', %s" % (expression, str(e))) "'%s', %s" % (expression, str(e)))
@timeit
def execute(self, task: SpiffTask, script, data): def execute(self, task: SpiffTask, script, data):
study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY] study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
if WorkflowProcessor.WORKFLOW_ID_KEY in task.workflow.data: if WorkflowProcessor.WORKFLOW_ID_KEY in task.workflow.data:
@ -70,6 +70,7 @@ class CustomBpmnScriptEngine(PythonScriptEngine):
if task.workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY]: if task.workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY]:
augment_methods = Script.generate_augmented_validate_list(task, study_id, workflow_id) augment_methods = Script.generate_augmented_validate_list(task, study_id, workflow_id)
else: else:
# Costs 0.25 seconds the first time it is executed.
augment_methods = Script.generate_augmented_list(task, study_id, workflow_id) augment_methods = Script.generate_augmented_list(task, study_id, workflow_id)
super().execute(task, script, data, external_methods=augment_methods) super().execute(task, script, data, external_methods=augment_methods)
except WorkflowException as e: except WorkflowException as e:
@ -78,10 +79,6 @@ class CustomBpmnScriptEngine(PythonScriptEngine):
raise WorkflowTaskExecException(task, f' {script}, {e}', e) raise WorkflowTaskExecException(task, f' {script}, {e}', e)
class MyCustomParser(BpmnDmnParser): class MyCustomParser(BpmnDmnParser):
""" """
A BPMN and DMN parser that can also parse Camunda forms. A BPMN and DMN parser that can also parse Camunda forms.
@ -110,7 +107,27 @@ class WorkflowProcessor(object):
raise (ApiError("missing_spec", "The spec this workflow references does not currently exist.")) raise (ApiError("missing_spec", "The spec this workflow references does not currently exist."))
self.spec_files = SpecFileService.get_files(spec_info, include_libraries=True) self.spec_files = SpecFileService.get_files(spec_info, include_libraries=True)
spec = self.get_spec(self.spec_files, spec_info) spec = self.get_spec(self.spec_files, spec_info)
else:
B = len(workflow_model.bpmn_workflow_json.encode('utf-8'))
MB = float(1024 ** 2)
json_size = B/MB
if json_size > 1:
wf_json = json.loads(workflow_model.bpmn_workflow_json)
task_tree = wf_json['task_tree']
test_spec = wf_json['wf_spec']
task_size = "{:.2f}".format(len(json.dumps(task_tree).encode('utf-8'))/MB)
spec_size = "{:.2f}".format(len(test_spec.encode('utf-8'))/MB)
task_specs = json.loads(test_spec)['task_specs']
sub_workflows = json.loads(test_spec)['sub_workflows']
message = 'Workflow ' + workflow_model.workflow_spec_id + ' JSON Size is over 1MB:{0:.2f} MB'.format(json_size)
message += f"\n Task Size: {task_size}"
message += f"\n Spec Size: {spec_size}"
message += f"\n Largest Sub-Process Sizes:"
for sw_name, sw_data in sub_workflows.items():
size = len(json.dumps(sw_data).encode('utf-8')) / MB
if size > 0.1:
message += "\n " + sw_name + " {:.2f}".format(size)
app.logger.warning(message)
self.workflow_spec_id = workflow_model.workflow_spec_id self.workflow_spec_id = workflow_model.workflow_spec_id
try: try:
@ -127,7 +144,6 @@ class WorkflowProcessor(object):
if self.WORKFLOW_ID_KEY not in self.bpmn_workflow.data: if self.WORKFLOW_ID_KEY not in self.bpmn_workflow.data:
if not workflow_model.id: if not workflow_model.id:
session.add(workflow_model) session.add(workflow_model)
session.commit()
# If the model is new, and has no id, save it, write it into the workflow model # If the model is new, and has no id, save it, write it into the workflow model
# and save it again. In this way, the workflow process is always aware of the # and save it again. In this way, the workflow process is always aware of the
# database model to which it is associated, and scripts running within the model # database model to which it is associated, and scripts running within the model
@ -135,6 +151,7 @@ class WorkflowProcessor(object):
self.bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = workflow_model.id self.bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = workflow_model.id
workflow_model.bpmn_workflow_json = WorkflowProcessor._serializer.serialize_workflow( workflow_model.bpmn_workflow_json = WorkflowProcessor._serializer.serialize_workflow(
self.bpmn_workflow, include_spec=True) self.bpmn_workflow, include_spec=True)
self.save() self.save()
except MissingSpecError as ke: except MissingSpecError as ke:
@ -145,11 +162,10 @@ class WorkflowProcessor(object):
@staticmethod @staticmethod
def reset(workflow_model, clear_data=False, delete_files=False): def reset(workflow_model, clear_data=False, delete_files=False):
# Try to execute a cancel notify # Try to execute a cancel notify
try: try:
wp = WorkflowProcessor(workflow_model) bpmn_workflow = WorkflowProcessor.__get_bpmn_workflow(workflow_model)
wp.cancel_notify() # The executes a notification to all endpoints that WorkflowProcessor.__cancel_notify(bpmn_workflow)
except Exception as e: except Exception as e:
app.logger.error(f"Unable to send a cancel notify for workflow %s during a reset." app.logger.error(f"Unable to send a cancel notify for workflow %s during a reset."
f" Continuing with the reset anyway so we don't get in an unresolvable" f" Continuing with the reset anyway so we don't get in an unresolvable"
@ -170,12 +186,14 @@ class WorkflowProcessor(object):
session.commit() session.commit()
return WorkflowProcessor(workflow_model) return WorkflowProcessor(workflow_model)
def __get_bpmn_workflow(self, workflow_model: WorkflowModel, spec: WorkflowSpec, validate_only=False): @staticmethod
def __get_bpmn_workflow(workflow_model: WorkflowModel, spec: WorkflowSpec = None, validate_only=False):
if workflow_model.bpmn_workflow_json: if workflow_model.bpmn_workflow_json:
bpmn_workflow = self._serializer.deserialize_workflow(workflow_model.bpmn_workflow_json, bpmn_workflow = WorkflowProcessor._serializer.deserialize_workflow(workflow_model.bpmn_workflow_json,
workflow_spec=spec) workflow_spec=spec)
bpmn_workflow.script_engine = WorkflowProcessor._script_engine
else: else:
bpmn_workflow = BpmnWorkflow(spec, script_engine=self._script_engine) bpmn_workflow = BpmnWorkflow(spec, script_engine=WorkflowProcessor._script_engine)
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = workflow_model.study_id bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = workflow_model.study_id
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = validate_only bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = validate_only
return bpmn_workflow return bpmn_workflow
@ -193,22 +211,16 @@ class WorkflowProcessor(object):
session.commit() session.commit()
@staticmethod @staticmethod
@timeit
def run_master_spec(spec_model, study): def run_master_spec(spec_model, study):
"""Executes a BPMN specification for the given study, without recording any information to the database """Executes a BPMN specification for the given study, without recording any information to the database
Useful for running the master specification, which should not persist. """ Useful for running the master specification, which should not persist. """
lasttime = firsttime()
spec_files = SpecFileService().get_files(spec_model, include_libraries=True) spec_files = SpecFileService().get_files(spec_model, include_libraries=True)
lasttime = sincetime('load Files', lasttime)
spec = WorkflowProcessor.get_spec(spec_files, spec_model) spec = WorkflowProcessor.get_spec(spec_files, spec_model)
lasttime = sincetime('get spec', lasttime)
try: try:
bpmn_workflow = BpmnWorkflow(spec, script_engine=WorkflowProcessor._script_engine) bpmn_workflow = BpmnWorkflow(spec, script_engine=WorkflowProcessor._script_engine)
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study.id bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study.id
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = False bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = False
lasttime = sincetime('get_workflow', lasttime)
bpmn_workflow.do_engine_steps() bpmn_workflow.do_engine_steps()
lasttime = sincetime('run steps', lasttime)
except WorkflowException as we: except WorkflowException as we:
raise ApiError.from_task_spec("error_running_master_spec", str(we), we.sender) raise ApiError.from_task_spec("error_running_master_spec", str(we), we.sender)
@ -275,14 +287,19 @@ class WorkflowProcessor(object):
raise ApiError.from_workflow_exception("task_error", str(we), we) raise ApiError.from_workflow_exception("task_error", str(we), we)
def cancel_notify(self): def cancel_notify(self):
self.__cancel_notify(self.bpmn_workflow)
@staticmethod
def __cancel_notify(bpmn_workflow):
try: try:
# A little hackly, but make the bpmn_workflow catch a cancel event. # A little hackly, but make the bpmn_workflow catch a cancel event.
self.bpmn_workflow.signal('cancel') # generate a cancel signal. bpmn_workflow.signal('cancel') # generate a cancel signal.
self.bpmn_workflow.catch(CancelEventDefinition()) bpmn_workflow.catch(CancelEventDefinition())
self.bpmn_workflow.do_engine_steps() bpmn_workflow.do_engine_steps()
except WorkflowTaskExecException as we: except WorkflowTaskExecException as we:
raise ApiError.from_workflow_exception("task_error", str(we), we) raise ApiError.from_workflow_exception("task_error", str(we), we)
def serialize(self): def serialize(self):
return self._serializer.serialize_workflow(self.bpmn_workflow,include_spec=True) return self._serializer.serialize_workflow(self.bpmn_workflow,include_spec=True)

View File

@ -19,7 +19,7 @@ from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.exceptions import WorkflowTaskExecException from SpiffWorkflow.exceptions import WorkflowTaskExecException
from SpiffWorkflow.specs import CancelTask, StartTask from SpiffWorkflow.specs import CancelTask, StartTask
from SpiffWorkflow.util.deep_merge import DeepMerge from SpiffWorkflow.util.deep_merge import DeepMerge
from SpiffWorkflow.util.metrics import timeit from SpiffWorkflow.util.metrics import timeit, firsttime, sincetime
from sqlalchemy.exc import InvalidRequestError from sqlalchemy.exc import InvalidRequestError
from crc import db, app, session from crc import db, app, session
@ -177,7 +177,6 @@ class WorkflowService(object):
raise ApiError(code='disabled_workflow', message=f"This workflow is disabled. {status[spec_id]['message']}") raise ApiError(code='disabled_workflow', message=f"This workflow is disabled. {status[spec_id]['message']}")
@staticmethod @staticmethod
@timeit
def test_spec(spec_id, validate_study_id=None, test_until=None, required_only=False): def test_spec(spec_id, validate_study_id=None, test_until=None, required_only=False):
"""Runs a spec through it's paces to see if it results in any errors. """Runs a spec through it's paces to see if it results in any errors.
Not fool-proof, but a good sanity check. Returns the final data Not fool-proof, but a good sanity check. Returns the final data
@ -256,6 +255,9 @@ class WorkflowService(object):
hide_groups = [] hide_groups = []
for field in task_api.form.fields:
form_data[field.id] = None
for field in task_api.form.fields: for field in task_api.form.fields:
# Assure we have a field type # Assure we have a field type
if field.type is None: if field.type is None:
@ -627,7 +629,6 @@ class WorkflowService(object):
def processor_to_workflow_api(processor: WorkflowProcessor, next_task=None): def processor_to_workflow_api(processor: WorkflowProcessor, next_task=None):
"""Returns an API model representing the state of the current workflow, if requested, and """Returns an API model representing the state of the current workflow, if requested, and
possible, next_task is set to the current_task.""" possible, next_task is set to the current_task."""
navigation = processor.bpmn_workflow.get_deep_nav_list() navigation = processor.bpmn_workflow.get_deep_nav_list()
WorkflowService.update_navigation(navigation, processor) WorkflowService.update_navigation(navigation, processor)
spec_service = WorkflowSpecService() spec_service = WorkflowSpecService()
@ -658,6 +659,7 @@ class WorkflowService(object):
user_uids = WorkflowService.get_users_assigned_to_task(processor, next_task) user_uids = WorkflowService.get_users_assigned_to_task(processor, next_task)
if not UserService.in_list(user_uids, allow_admin_impersonate=True): if not UserService.in_list(user_uids, allow_admin_impersonate=True):
workflow_api.next_task.state = WorkflowService.TASK_STATE_LOCKED workflow_api.next_task.state = WorkflowService.TASK_STATE_LOCKED
return workflow_api return workflow_api
@staticmethod @staticmethod
@ -666,9 +668,7 @@ class WorkflowService(object):
for nav_item in navigation: for nav_item in navigation:
spiff_task = processor.bpmn_workflow.get_task(nav_item.task_id) spiff_task = processor.bpmn_workflow.get_task(nav_item.task_id)
if spiff_task: if spiff_task:
# Use existing logic to set the description, and alter the state based on permissions. nav_item.description = WorkflowService.__calculate_title(spiff_task)
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) 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)) \ 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): and not UserService.in_list(user_uids, allow_admin_impersonate=True):
@ -777,13 +777,23 @@ class WorkflowService(object):
if spiff_task.state == SpiffTask.READY: if spiff_task.state == SpiffTask.READY:
task.properties = WorkflowService._process_properties(spiff_task, props) task.properties = WorkflowService._process_properties(spiff_task, props)
# Replace the title with the display name if it is set in the task properties, task.title = WorkflowService.__calculate_title(spiff_task)
# otherwise strip off the first word of the task, as that should be following
# a BPMN standard, and should not be included in the display. if task.properties and "clear_data" in task.properties:
if task.properties and "display_name" in task.properties: if task.form and task.properties['clear_data'] == 'True':
for i in range(len(task.form.fields)):
task.data.pop(task.form.fields[i].id, None)
return task
@staticmethod
def __calculate_title(spiff_task):
title = spiff_task.task_spec.description or None
if hasattr(spiff_task.task_spec, 'extensions') and "display_name" in spiff_task.task_spec.extensions:
title = spiff_task.task_spec.extensions["display_name"]
try: try:
task.title = spiff_task.workflow.script_engine.evaluate(spiff_task, title = JinjaService.get_content(title, spiff_task.data)
task.properties[Task.PROP_EXTENSIONS_TITLE]) title = spiff_task.workflow.script_engine.evaluate(spiff_task, title)
except Exception as e: except Exception as e:
# if the task is ready, we should raise an error, but if it is in the future or the past, we may not # if the task is ready, we should raise an error, but if it is in the future or the past, we may not
# have the information we need to properly set the title, so don't error out, and just use what is # have the information we need to properly set the title, so don't error out, and just use what is
@ -793,16 +803,10 @@ class WorkflowService(object):
message="Could not set task title on task %s with '%s' property because %s" % message="Could not set task title on task %s with '%s' property because %s" %
(spiff_task.task_spec.name, Task.PROP_EXTENSIONS_TITLE, str(e)), (spiff_task.task_spec.name, Task.PROP_EXTENSIONS_TITLE, str(e)),
task=spiff_task) task=spiff_task)
# Otherwise, just use the curreent title. elif title and ' ' in title:
elif task.title and ' ' in task.title: title = title.partition(' ')[2]
task.title = task.title.partition(' ')[2] return title
if task.properties and "clear_data" in task.properties:
if task.form and task.properties['clear_data'] == 'True':
for i in range(len(task.form.fields)):
task.data.pop(task.form.fields[i].id, None)
return task
@staticmethod @staticmethod
def _process_properties(spiff_task, props): def _process_properties(spiff_task, props):
@ -839,7 +843,10 @@ class WorkflowService(object):
try: try:
return JinjaService.get_content(raw_doc, spiff_task.data) return JinjaService.get_content(raw_doc, spiff_task.data)
except jinja2.exceptions.TemplateSyntaxError as tse: except jinja2.exceptions.TemplateSyntaxError as tse:
error_line = documentation.splitlines()[tse.lineno - 1] lines = tse.source.splitlines()
error_line = ""
if len(lines) >= tse.lineno - 1:
error_line = tse.source.splitlines()[tse.lineno - 1]
raise ApiError.from_task(code="template_error", message="Jinja Template Error: %s" % str(tse), raise ApiError.from_task(code="template_error", message="Jinja Template Error: %s" % str(tse),
task=spiff_task, line_number=tse.lineno, error_line=error_line) task=spiff_task, line_number=tse.lineno, error_line=error_line)
except jinja2.exceptions.TemplateError as te: except jinja2.exceptions.TemplateError as te:
@ -926,9 +933,8 @@ class WorkflowService(object):
db.session.query(TaskEventModel). \ db.session.query(TaskEventModel). \
filter(TaskEventModel.workflow_id == processor.workflow_model.id). \ filter(TaskEventModel.workflow_id == processor.workflow_model.id). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).delete() filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).delete()
db.session.commit() tasks = processor.get_current_user_tasks()
for task in tasks:
for task in processor.get_current_user_tasks():
user_ids = WorkflowService.get_users_assigned_to_task(processor, task) user_ids = WorkflowService.get_users_assigned_to_task(processor, task)
for user_id in user_ids: for user_id in user_ids:
WorkflowService.log_task_action(user_id, processor, task, WorkflowService.TASK_ACTION_ASSIGNMENT) WorkflowService.log_task_action(user_id, processor, task, WorkflowService.TASK_ACTION_ASSIGNMENT)
@ -996,7 +1002,6 @@ class WorkflowService(object):
# date=datetime.utcnow(), <=== For future reference, NEVER do this. Let the database set the time. # date=datetime.utcnow(), <=== For future reference, NEVER do this. Let the database set the time.
) )
db.session.add(task_event) db.session.add(task_event)
db.session.commit()
@staticmethod @staticmethod
def extract_form_data(latest_data, task): def extract_form_data(latest_data, task):

View File

@ -0,0 +1,39 @@
<?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_0g5rfar" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_05y6lb7">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0djwo51</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0djwo51" sourceRef="StartEvent_1" targetRef="Activity_1gst0w5" />
<bpmn:endEvent id="Event_1fetqlx">
<bpmn:incoming>Flow_19hnovy</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_19hnovy" sourceRef="Activity_1gst0w5" targetRef="Event_1fetqlx" />
<bpmn:manualTask id="Activity_1gst0w5" name="Hello World">
<bpmn:documentation>## Hello World</bpmn:documentation>
<bpmn:incoming>Flow_0djwo51</bpmn:incoming>
<bpmn:outgoing>Flow_19hnovy</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_05y6lb7">
<bpmndi:BPMNEdge id="Flow_0djwo51_di" bpmnElement="Flow_0djwo51">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_19hnovy_di" bpmnElement="Flow_19hnovy">
<di:waypoint x="370" y="117" />
<di:waypoint x="432" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1fetqlx_di" bpmnElement="Event_1fetqlx">
<dc:Bounds x="432" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_07hce4z_di" bpmnElement="Activity_1gst0w5">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,88 @@
<?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_1wrf54p" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_ModifySpreadsheet" name="Modify Spreadsheet" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_03wyga3</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_03wyga3" sourceRef="StartEvent_1" targetRef="Activity_FileUpload" />
<bpmn:sequenceFlow id="Flow_0msewj5" sourceRef="Activity_FileUpload" targetRef="Activity_GetModifyData" />
<bpmn:sequenceFlow id="Flow_0fpc32g" sourceRef="Activity_GetModifyData" targetRef="Activity_ModifySpreadsheet" />
<bpmn:sequenceFlow id="Flow_10ulj3l" sourceRef="Activity_ModifySpreadsheet" targetRef="Event_1d07dls" />
<bpmn:endEvent id="Event_1d07dls">
<bpmn:incoming>Flow_10ulj3l</bpmn:incoming>
</bpmn:endEvent>
<bpmn:userTask id="Activity_FileUpload" name="Upload Spreadsheet" camunda:formKey="FileUpload">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="Finance_BCA" label="&#39;File Upload&#39;" type="file">
<camunda:validation>
<camunda:constraint name="required" config="True" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_03wyga3</bpmn:incoming>
<bpmn:outgoing>Flow_0msewj5</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Activity_ModifySpreadsheet" name="Modify Spreadsheet">
<bpmn:incoming>Flow_0fpc32g</bpmn:incoming>
<bpmn:outgoing>Flow_10ulj3l</bpmn:outgoing>
<bpmn:script>modify_spreadsheet(irb_doc_code='Finance_BCA', cell=cell_indicator, text=input_text)</bpmn:script>
</bpmn:scriptTask>
<bpmn:userTask id="Activity_GetModifyData" name="Get Modify Data" camunda:formKey="ModifyData">
<bpmn:documentation>## File Upload
{{ Finance_BCA }}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="cell_indicator" label="Cell Indicator" type="string">
<camunda:validation>
<camunda:constraint name="required" config="True" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="input_text" label="Input Text" type="string">
<camunda:validation>
<camunda:constraint name="required" config="True" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0msewj5</bpmn:incoming>
<bpmn:outgoing>Flow_0fpc32g</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_ModifySpreadsheet">
<bpmndi:BPMNEdge id="Flow_10ulj3l_di" bpmnElement="Flow_10ulj3l">
<di:waypoint x="691" y="117" />
<di:waypoint x="912" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0fpc32g_di" bpmnElement="Flow_0fpc32g">
<di:waypoint x="530" y="117" />
<di:waypoint x="591" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0msewj5_di" bpmnElement="Flow_0msewj5">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_03wyga3_di" bpmnElement="Flow_03wyga3">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1d07dls_di" bpmnElement="Event_1d07dls">
<dc:Bounds x="912" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0jut75d_di" bpmnElement="Activity_FileUpload">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0uk32ds_di" bpmnElement="Activity_ModifySpreadsheet">
<dc:Bounds x="591" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0hn4ju2_di" bpmnElement="Activity_GetModifyData">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

Binary file not shown.

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?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_00dbd41" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.0.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_00dbd41" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.10.0">
<bpmn:process id="Process_SetStudyStatus" name="Set Study Status" isExecutable="true"> <bpmn:process id="Process_SetStudyStatus" name="Set Study Status" isExecutable="true">
<bpmn:startEvent id="StartEvent_1"> <bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0c77bdh</bpmn:outgoing> <bpmn:outgoing>Flow_0c77bdh</bpmn:outgoing>
@ -7,7 +7,10 @@
<bpmn:userTask id="Activity_GetSelectedStatus" name="Get Selected Status" camunda:formKey="SelectStudyStatus"> <bpmn:userTask id="Activity_GetSelectedStatus" name="Get Selected Status" camunda:formKey="SelectStudyStatus">
<bpmn:extensionElements> <bpmn:extensionElements>
<camunda:formData> <camunda:formData>
<camunda:formField id="selected_status" label="'Select Study Status'" type="enum"> <camunda:formField id="selected_status" label="&#39;Select Study Status&#39;" type="enum">
<camunda:validation>
<camunda:constraint name="required" config="True" />
</camunda:validation>
<camunda:value id="approved" name="Approved" /> <camunda:value id="approved" name="Approved" />
<camunda:value id="disapproved" name="Disapproved" /> <camunda:value id="disapproved" name="Disapproved" />
</camunda:formField> </camunda:formField>

View File

@ -0,0 +1,76 @@
<?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_0a7bvlf" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<bpmn:process id="Process_0inkg2m" name="Start Workflow Programmatically" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0ac3s7d</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0ac3s7d" sourceRef="StartEvent_1" targetRef="Activity_0n0md5g" />
<bpmn:sequenceFlow id="Flow_03vp8ep" sourceRef="Activity_0n0md5g" targetRef="Activity_0qxilu1" />
<bpmn:sequenceFlow id="Flow_0etvuwr" sourceRef="Activity_0qxilu1" targetRef="Activity_1thn0qo" />
<bpmn:endEvent id="Event_1hlx8d1">
<bpmn:incoming>Flow_1ppd3wf</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1ppd3wf" sourceRef="Activity_1thn0qo" targetRef="Event_1hlx8d1" />
<bpmn:userTask id="Activity_0n0md5g" name="Get Required Data" camunda:formKey="DataForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="workflow_spec_to_start" label="&#39;Workflow Spec&#39;" type="string">
<camunda:validation>
<camunda:constraint name="required" config="True" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0ac3s7d</bpmn:incoming>
<bpmn:outgoing>Flow_03vp8ep</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Activity_0qxilu1" name="Start Workflow ">
<bpmn:incoming>Flow_03vp8ep</bpmn:incoming>
<bpmn:outgoing>Flow_0etvuwr</bpmn:outgoing>
<bpmn:script>workflow_api = start_workflow(workflow_spec_id=workflow_spec_to_start)</bpmn:script>
</bpmn:scriptTask>
<bpmn:manualTask id="Activity_1thn0qo" name="Display Result">
<bpmn:documentation>## Result
{{ workflow_api }}</bpmn:documentation>
<bpmn:incoming>Flow_0etvuwr</bpmn:incoming>
<bpmn:outgoing>Flow_1ppd3wf</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0inkg2m">
<bpmndi:BPMNEdge id="Flow_1ppd3wf_di" bpmnElement="Flow_1ppd3wf">
<di:waypoint x="690" y="117" />
<di:waypoint x="752" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0etvuwr_di" bpmnElement="Flow_0etvuwr">
<di:waypoint x="530" y="117" />
<di:waypoint x="590" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_03vp8ep_di" bpmnElement="Flow_03vp8ep">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ac3s7d_di" bpmnElement="Flow_0ac3s7d">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1hlx8d1_di" bpmnElement="Event_1hlx8d1">
<dc:Bounds x="752" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0gf4hes_di" bpmnElement="Activity_0n0md5g">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_11ydp76_di" bpmnElement="Activity_0qxilu1">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1sy9ble_di" bpmnElement="Activity_1thn0qo">
<dc:Bounds x="590" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -35,7 +35,7 @@ class TestEmailScript(BaseTest):
self.assertIn('<strong>Test Some Formatting</strong><br />', outbox[0].html) self.assertIn('<strong>Test Some Formatting</strong><br />', outbox[0].html)
# Correct From field # Correct From field
self.assertEqual('uvacrconnect@virginia.edu', outbox[0].sender) self.assertEqual('crconnect2@virginia.edu', outbox[0].sender)
# Make sure we log the email # Make sure we log the email
# Grab model for comparison below # Grab model for comparison below

View File

@ -0,0 +1,78 @@
from tests.base_test import BaseTest
from crc import app, session
from crc.models.file import FileModel, FileDataModel
from crc.services.user_file_service import UserFileService
from io import BytesIO
from openpyxl import load_workbook
import os
class TestModifySpreadsheet(BaseTest):
def upload_spreadsheet(self, workflow, task, irb_doc_code):
filepath = os.path.join(app.root_path, '..', 'tests', 'data',
'modify_spreadsheet', 'test_spreadsheet.xlsx')
with open(filepath, 'br') as f_open:
ss_data = f_open.read()
file_model = UserFileService.add_workflow_file(workflow_id=workflow.id,
task_spec_name=task.name,
name="test_spreadsheet.xlsx", content_type="text",
binary_data=ss_data, irb_doc_code=irb_doc_code)
workflow_api = self.complete_form(workflow, task, {irb_doc_code: {'id': file_model.id}})
return workflow_api
@staticmethod
def get_sheet(workflow_id, irb_doc_code):
spreadsheet = session.query(FileModel). \
filter(FileModel.workflow_id == workflow_id). \
filter(FileModel.irb_doc_code == irb_doc_code). \
first()
spreadsheet_data = session.query(FileDataModel). \
filter(FileDataModel.file_model_id == spreadsheet.id). \
first()
workbook = load_workbook(BytesIO(spreadsheet_data.data))
sheet = workbook.active
return sheet
def test_modify_spreadsheet(self):
irb_doc_code = 'Finance_BCA'
cell_indicator = 'C4'
input_text = 'This is my input text.'
workflow = self.create_workflow('modify_spreadsheet')
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
workflow_api = self.upload_spreadsheet(workflow, task, irb_doc_code)
sheet = self.get_sheet(workflow.id, irb_doc_code)
self.assertEqual(None, sheet[cell_indicator].value)
task = workflow_api.next_task
self.complete_form(workflow, task, {'cell_indicator': cell_indicator,
'input_text': input_text})
sheet = self.get_sheet(workflow.id, irb_doc_code)
self.assertEqual(input_text, sheet[cell_indicator].value)
def test_missing_spreadsheet(self):
"""The modify_spreadsheet has Finance_BCA hard-coded as the spreadsheet to modify.
In this test we upload a spreadsheet with a different doc code,
and assert that we raise an error when the Finance_BCA spreadsheet does not exist"""
irb_doc_code = 'Finance_GPRF'
cell_indicator = 'C4'
input_text = 'This is my input text.'
workflow = self.create_workflow('modify_spreadsheet')
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
workflow_api = self.upload_spreadsheet(workflow, task, irb_doc_code)
task = workflow_api.next_task
with self.assertRaises(AssertionError):
self.complete_form(workflow, task, {'cell_indicator': cell_indicator,
'input_text': input_text})

View File

@ -0,0 +1,47 @@
from tests.base_test import BaseTest
from crc import session
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowModel
from crc.services.study_service import StudyService
from crc.services.workflow_spec_service import WorkflowSpecService
class TestStartWorkflow(BaseTest):
def setup_test_start_workflow(self):
self.add_users()
self.create_workflow('hello_world')
workflow_spec_to_start = WorkflowSpecService().get_spec('hello_world')
workflow = self.create_workflow('start_workflow')
study_id = workflow.study_id
study = session.query(StudyModel).filter(StudyModel.id==study_id).first()
StudyService.add_all_workflow_specs_to_study(study, [workflow_spec_to_start])
return workflow
def test_start_workflow_validation(self):
spec_model = self.load_test_spec('start_workflow')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
self.assertEqual([], rv.json)
def test_start_workflow(self):
workflow = self.setup_test_start_workflow()
workflow_before = session.query(WorkflowModel).filter(WorkflowModel.workflow_spec_id=='hello_world').first()
self.assertEqual('not_started', workflow_before.status.value)
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
self.complete_form(workflow, task, {'workflow_spec_to_start': 'hello_world'})
workflow_after = session.query(WorkflowModel).filter(WorkflowModel.workflow_spec_id=='hello_world').first()
self.assertEqual('user_input_required', workflow_after.status.value)
def test_bad_workflow_spec_id(self):
workflow = self.setup_test_start_workflow()
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
with self.assertRaises(AssertionError) as e:
self.complete_form(workflow, task, {'workflow_spec_to_start': 'bad_spec_id'})

View File

@ -107,6 +107,7 @@ class TestStudyDetailsDocumentsScript(BaseTest):
FileDataSet().do_task(task, study.id, workflow_model.id, key="ginger", value="doodle", file_id=file.id) FileDataSet().do_task(task, study.id, workflow_model.id, key="ginger", value="doodle", file_id=file.id)
docs = StudyInfo().do_task(task, study.id, workflow_model.id, "documents") docs = StudyInfo().do_task(task, study.id, workflow_model.id, "documents")
self.assertTrue(isinstance(docs, Box)) self.assertTrue(isinstance(docs, Box))
docs = StudyService.get_documents_status(study.id, force=True)
self.assertEqual(1, len(docs.UVACompl_PRCAppr.files)) self.assertEqual(1, len(docs.UVACompl_PRCAppr.files))
self.assertEqual("doodle", docs.UVACompl_PRCAppr.files[0].data_store.ginger) self.assertEqual("doodle", docs.UVACompl_PRCAppr.files[0].data_store.ginger)

View File

@ -107,7 +107,7 @@ class TestStudyService(BaseTest):
study_service = StudyService() study_service = StudyService()
documents = study_service.get_documents_status(study_id=study.id) # Mocked out, any random study id works. documents = study_service.get_documents_status(study_id=study.id, force=True) # Mocked out, any random study id works.
self.assertIsNotNone(documents) self.assertIsNotNone(documents)
self.assertTrue("UVACompl_PRCAppr" in documents.keys()) self.assertTrue("UVACompl_PRCAppr" in documents.keys())
self.assertEqual("UVACompl_PRCAppr", documents["UVACompl_PRCAppr"]['code']) self.assertEqual("UVACompl_PRCAppr", documents["UVACompl_PRCAppr"]['code'])

View File

@ -6,6 +6,7 @@ from crc.scripts.study_info import StudyInfo
from crc import app from crc import app
from unittest.mock import patch from unittest.mock import patch
from crc.services.protocol_builder import ProtocolBuilderService from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.study_service import StudyService
class TestStudyInfoScript(BaseTest): class TestStudyInfoScript(BaseTest):
@ -100,6 +101,8 @@ class TestStudyInfoScript(BaseTest):
content_type='multipart/form-data', headers=self.logged_in_headers()) content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
file_data = json.loads(rv.get_data(as_text=True)) file_data = json.loads(rv.get_data(as_text=True))
study_info = StudyService.get_documents_status(self.workflow.study_id, force=True)
self.assertEqual(1, len(study_info['Grant_App']['files']), "Grant_App has exactly one file.")
# Now get the study info again. # Now get the study info again.
study_info = StudyInfo().do_task(self.workflow_api.study_id, self.workflow.study.id, self.workflow.id, study_info = StudyInfo().do_task(self.workflow_api.study_id, self.workflow.study.id, self.workflow.id,

View File

@ -0,0 +1,12 @@
from tests.base_test import BaseTest
from crc.api.common import ApiError
class TestMissingExecutable(BaseTest):
def test_missing_executable(self):
with self.assertRaises(ApiError) as ae:
self.create_workflow('missing_executable_tag')
self.assertEqual('No executable process tag found. Please make sure the Executable option is set in the workflow.',
ae.exception.message)