Merge pull request #167 from sartography/staging

Staging -> Master
This commit is contained in:
Aaron Louie 2020-07-27 09:04:59 -04:00 committed by GitHub
commit 1be985fe0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 4158 additions and 1850 deletions

40
Pipfile
View File

@ -9,38 +9,42 @@ pbr = "*"
coverage = "*"
[packages]
alembic = "*"
connexion = {extras = ["swagger-ui"],version = "*"}
swagger-ui-bundle = "*"
coverage = "*"
docxtpl = "*"
flask = "*"
flask-admin = "*"
flask-bcrypt = "*"
flask-cors = "*"
flask-mail = "*"
flask-marshmallow = "*"
flask-migrate = "*"
flask-restful = "*"
gunicorn = "*"
httpretty = "*"
ldap3 = "*"
lxml = "*"
markdown = "*"
marshmallow = "*"
marshmallow-enum = "*"
marshmallow-sqlalchemy = "*"
openpyxl = "*"
pyjwt = "*"
requests = "*"
xlsxwriter = "*"
webtest = "*"
spiffworkflow = {editable = true,git = "https://github.com/sartography/SpiffWorkflow.git",ref = "deploy"}
alembic = "*"
coverage = "*"
sphinx = "*"
recommonmark = "*"
psycopg2-binary = "*"
docxtpl = "*"
python-dateutil = "*"
pandas = "*"
xlrd = "*"
ldap3 = "*"
gunicorn = "*"
werkzeug = "*"
psycopg2-binary = "*"
pyjwt = "*"
python-dateutil = "*"
recommonmark = "*"
requests = "*"
sentry-sdk = {extras = ["flask"],version = "==0.14.4"}
flask-mail = "*"
sphinx = "*"
spiffworkflow = {editable = true,git = "https://github.com/sartography/SpiffWorkflow.git",ref = "master"}
#spiffworkflow = {editable = true,path="/home/kelly/sartography/SpiffWorkflow/"}
swagger-ui-bundle = "*"
webtest = "*"
werkzeug = "*"
xlrd = "*"
xlsxwriter = "*"
[requires]
python_version = "3.7"

508
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "faaf0e1f31f4bf99df366e52df20bb148a05996a0e6467767660665c514af2d7"
"sha256": "97a15c4ade88db2b384d52436633889a4d9b0bdcaeea86b8a679ebda6f73fb59"
},
"pipfile-spec": 6,
"requires": {
@ -104,17 +104,17 @@
},
"celery": {
"hashes": [
"sha256:c3f4173f83ceb5a5c986c5fdaefb9456de3b0729a72a5776e46bd405fda7b647",
"sha256:d1762d6065522879f341c3d67c2b9fe4615eb79756d59acb1434601d4aca474b"
"sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916",
"sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"
],
"version": "==4.4.5"
"version": "==4.4.6"
},
"certifi": {
"hashes": [
"sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1",
"sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.4.5.2"
"version": "==2020.6.20"
},
"cffi": {
"hashes": [
@ -197,40 +197,43 @@
},
"coverage": {
"hashes": [
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
"sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d",
"sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2",
"sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703",
"sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404",
"sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7",
"sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405",
"sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d",
"sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c",
"sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6",
"sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70",
"sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40",
"sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4",
"sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613",
"sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10",
"sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b",
"sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0",
"sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec",
"sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1",
"sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d",
"sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913",
"sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e",
"sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62",
"sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e",
"sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a",
"sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d",
"sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f",
"sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e",
"sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b",
"sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c",
"sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032",
"sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a",
"sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee",
"sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c",
"sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"
],
"index": "pypi",
"version": "==5.1"
"version": "==5.2"
},
"docutils": {
"hashes": [
@ -261,6 +264,13 @@
"index": "pypi",
"version": "==1.1.2"
},
"flask-admin": {
"hashes": [
"sha256:68c761d8582d59b1f7702013e944a7ad11d7659a72f3006b89b68b0bd8df61b8"
],
"index": "pypi",
"version": "==1.5.6"
},
"flask-bcrypt": {
"hashes": [
"sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f"
@ -309,10 +319,10 @@
},
"flask-sqlalchemy": {
"hashes": [
"sha256:0b656fbf87c5f24109d859bafa791d29751fabbda2302b606881ae5485b557a5",
"sha256:fcfe6df52cd2ed8a63008ca36b86a51fa7a4b70cef1c39e5625f722fca32308e"
"sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
"sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"
],
"version": "==2.4.3"
"version": "==2.4.4"
},
"future": {
"hashes": [
@ -337,10 +347,10 @@
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.9"
"version": "==2.10"
},
"imagesize": {
"hashes": [
@ -351,11 +361,11 @@
},
"importlib-metadata": {
"hashes": [
"sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545",
"sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
],
"markers": "python_version < '3.8'",
"version": "==1.6.1"
"version": "==1.7.0"
},
"inflection": {
"hashes": [
@ -394,10 +404,10 @@
},
"kombu": {
"hashes": [
"sha256:437b9cdea193cc2ed0b8044c85fd0f126bb3615ca2f4d4a35b39de7cacfa3c1a",
"sha256:dc282bb277197d723bccda1a9ba30a27a28c9672d0ab93e9e51bb05a37bd29c3"
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
],
"version": "==4.6.10"
"version": "==4.6.11"
},
"ldap3": {
"hashes": [
@ -409,35 +419,40 @@
},
"lxml": {
"hashes": [
"sha256:06748c7192eab0f48e3d35a7adae609a329c6257495d5e53878003660dc0fec6",
"sha256:0790ddca3f825dd914978c94c2545dbea5f56f008b050e835403714babe62a5f",
"sha256:1aa7a6197c1cdd65d974f3e4953764eee3d9c7b67e3966616b41fab7f8f516b7",
"sha256:22c6d34fdb0e65d5f782a4d1a1edb52e0a8365858dafb1c08cb1d16546cf0786",
"sha256:2754d4406438c83144f9ffd3628bbe2dcc6d62b20dbc5c1ec4bc4385e5d44b42",
"sha256:27ee0faf8077c7c1a589573b1450743011117f1aa1a91d5ae776bbc5ca6070f2",
"sha256:2b02c106709466a93ed424454ce4c970791c486d5fcdf52b0d822a7e29789626",
"sha256:2d1ddce96cf15f1254a68dba6935e6e0f1fe39247de631c115e84dd404a6f031",
"sha256:4f282737d187ae723b2633856085c31ae5d4d432968b7f3f478a48a54835f5c4",
"sha256:51bb4edeb36d24ec97eb3e6a6007be128b720114f9a875d6b370317d62ac80b9",
"sha256:7eee37c1b9815e6505847aa5e68f192e8a1b730c5c7ead39ff317fde9ce29448",
"sha256:7fd88cb91a470b383aafad554c3fe1ccf6dfb2456ff0e84b95335d582a799804",
"sha256:9144ce36ca0824b29ebc2e02ca186e54040ebb224292072250467190fb613b96",
"sha256:925baf6ff1ef2c45169f548cc85204433e061360bfa7d01e1be7ae38bef73194",
"sha256:a636346c6c0e1092ffc202d97ec1843a75937d8c98aaf6771348ad6422e44bb0",
"sha256:a87dbee7ad9dce3aaefada2081843caf08a44a8f52e03e0a4cc5819f8398f2f4",
"sha256:a9e3b8011388e7e373565daa5e92f6c9cb844790dc18e43073212bb3e76f7007",
"sha256:afb53edf1046599991fb4a7d03e601ab5f5422a5435c47ee6ba91ec3b61416a6",
"sha256:b26719890c79a1dae7d53acac5f089d66fd8cc68a81f4e4bd355e45470dc25e1",
"sha256:b7462cdab6fffcda853338e1741ce99706cdf880d921b5a769202ea7b94e8528",
"sha256:b77975465234ff49fdad871c08aa747aae06f5e5be62866595057c43f8d2f62c",
"sha256:c47a8a5d00060122ca5908909478abce7bbf62d812e3fc35c6c802df8fb01fe7",
"sha256:c79e5debbe092e3c93ca4aee44c9a7631bdd407b2871cb541b979fd350bbbc29",
"sha256:d8d40e0121ca1606aa9e78c28a3a7d88a05c06b3ca61630242cded87d8ce55fa",
"sha256:ee2be8b8f72a2772e72ab926a3bccebf47bb727bda41ae070dc91d1fb759b726",
"sha256:f95d28193c3863132b1f55c1056036bf580b5a488d908f7d22a04ace8935a3a9",
"sha256:fadd2a63a2bfd7fb604508e553d1cf68eca250b2fbdbd81213b5f6f2fbf23529"
"sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f",
"sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730",
"sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f",
"sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1",
"sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3",
"sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7",
"sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a",
"sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe",
"sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1",
"sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e",
"sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d",
"sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20",
"sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae",
"sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5",
"sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba",
"sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293",
"sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a",
"sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6",
"sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88",
"sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed",
"sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843",
"sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443",
"sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0",
"sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304",
"sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258",
"sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6",
"sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1",
"sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481",
"sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef",
"sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd",
"sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"
],
"version": "==4.5.1"
"index": "pypi",
"version": "==4.5.2"
},
"mako": {
"hashes": [
@ -446,6 +461,14 @@
],
"version": "==1.1.3"
},
"markdown": {
"hashes": [
"sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17",
"sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"
],
"index": "pypi",
"version": "==3.2.2"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
@ -486,11 +509,11 @@
},
"marshmallow": {
"hashes": [
"sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5",
"sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f"
"sha256:67bf4cae9d3275b3fc74bd7ff88a7c98ee8c57c94b251a67b031dc293ecc4b76",
"sha256:a2a5eefb4b75a3b43f05be1cca0b6686adf56af7465c3ca629e5ad8d1e1fe13d"
],
"index": "pypi",
"version": "==3.6.1"
"version": "==3.7.1"
},
"marshmallow-enum": {
"hashes": [
@ -510,29 +533,34 @@
},
"numpy": {
"hashes": [
"sha256:0172304e7d8d40e9e49553901903dc5f5a49a703363ed756796f5808a06fc233",
"sha256:34e96e9dae65c4839bd80012023aadd6ee2ccb73ce7fdf3074c62f301e63120b",
"sha256:3676abe3d621fc467c4c1469ee11e395c82b2d6b5463a9454e37fe9da07cd0d7",
"sha256:3dd6823d3e04b5f223e3e265b4a1eae15f104f4366edd409e5a5e413a98f911f",
"sha256:4064f53d4cce69e9ac613256dc2162e56f20a4e2d2086b1956dd2fcf77b7fac5",
"sha256:4674f7d27a6c1c52a4d1aa5f0881f1eff840d2206989bae6acb1c7668c02ebfb",
"sha256:7d42ab8cedd175b5ebcb39b5208b25ba104842489ed59fbb29356f671ac93583",
"sha256:965df25449305092b23d5145b9bdaeb0149b6e41a77a7d728b1644b3c99277c1",
"sha256:9c9d6531bc1886454f44aa8f809268bc481295cf9740827254f53c30104f074a",
"sha256:a78e438db8ec26d5d9d0e584b27ef25c7afa5a182d1bf4d05e313d2d6d515271",
"sha256:a7acefddf994af1aeba05bbbafe4ba983a187079f125146dc5859e6d817df824",
"sha256:a87f59508c2b7ceb8631c20630118cc546f1f815e034193dc72390db038a5cb3",
"sha256:ac792b385d81151bae2a5a8adb2b88261ceb4976dbfaaad9ce3a200e036753dc",
"sha256:b03b2c0badeb606d1232e5f78852c102c0a7989d3a534b3129e7856a52f3d161",
"sha256:b39321f1a74d1f9183bf1638a745b4fd6fe80efbb1f6b32b932a588b4bc7695f",
"sha256:cae14a01a159b1ed91a324722d746523ec757357260c6804d11d6147a9e53e3f",
"sha256:cd49930af1d1e49a812d987c2620ee63965b619257bd76eaaa95870ca08837cf",
"sha256:e15b382603c58f24265c9c931c9a45eebf44fe2e6b4eaedbb0d025ab3255228b",
"sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0",
"sha256:ef627986941b5edd1ed74ba89ca43196ed197f1a206a3f18cc9faf2fb84fd675",
"sha256:f718a7949d1c4f622ff548c572e0c03440b49b9531ff00e4ed5738b459f011e8"
"sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983",
"sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065",
"sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968",
"sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132",
"sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129",
"sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff",
"sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93",
"sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a",
"sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7",
"sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd",
"sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055",
"sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc",
"sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7",
"sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624",
"sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b",
"sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69",
"sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491",
"sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954",
"sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72",
"sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7",
"sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae",
"sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1",
"sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a",
"sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e",
"sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e",
"sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"
],
"version": "==1.18.5"
"version": "==1.19.1"
},
"openapi-spec-validator": {
"hashes": [
@ -544,10 +572,11 @@
},
"openpyxl": {
"hashes": [
"sha256:547a9fc6aafcf44abe358b89ed4438d077e9d92e4f182c87e2dc294186dc4b64"
"sha256:6e62f058d19b09b95d20ebfbfb04857ad08d0833190516c1660675f699c6186f",
"sha256:d88dd1480668019684c66cfff3e52a5de4ed41e9df5dd52e008cbf27af0dbf87"
],
"index": "pypi",
"version": "==3.0.3"
"version": "==3.0.4"
},
"packaging": {
"hashes": [
@ -558,25 +587,25 @@
},
"pandas": {
"hashes": [
"sha256:034185bb615dc96d08fa13aacba8862949db19d5e7804d6ee242d086f07bcc46",
"sha256:0c9b7f1933e3226cc16129cf2093338d63ace5c85db7c9588e3e1ac5c1937ad5",
"sha256:1f6fcf0404626ca0475715da045a878c7062ed39bc859afc4ccf0ba0a586a0aa",
"sha256:1fc963ba33c299973e92d45466e576d11f28611f3549469aec4a35658ef9f4cc",
"sha256:29b4cfee5df2bc885607b8f016e901e63df7ffc8f00209000471778f46cc6678",
"sha256:2a8b6c28607e3f3c344fe3e9b3cd76d2bf9f59bc8c0f2e582e3728b80e1786dc",
"sha256:2bc2ff52091a6ac481cc75d514f06227dc1b10887df1eb72d535475e7b825e31",
"sha256:415e4d52fcfd68c3d8f1851cef4d947399232741cc994c8f6aa5e6a9f2e4b1d8",
"sha256:519678882fd0587410ece91e3ff7f73ad6ded60f6fcb8aa7bcc85c1dc20ecac6",
"sha256:51e0abe6e9f5096d246232b461649b0aa627f46de8f6344597ca908f2240cbaa",
"sha256:698e26372dba93f3aeb09cd7da2bb6dd6ade248338cfe423792c07116297f8f4",
"sha256:83af85c8e539a7876d23b78433d90f6a0e8aa913e37320785cf3888c946ee874",
"sha256:982cda36d1773076a415ec62766b3c0a21cdbae84525135bdb8f460c489bb5dd",
"sha256:a647e44ba1b3344ebc5991c8aafeb7cca2b930010923657a273b41d86ae225c4",
"sha256:b35d625282baa7b51e82e52622c300a1ca9f786711b2af7cbe64f1e6831f4126",
"sha256:bab51855f8b318ef39c2af2c11095f45a10b74cbab4e3c8199efcc5af314c648"
"sha256:02f1e8f71cd994ed7fcb9a35b6ddddeb4314822a0e09a9c5b2d278f8cb5d4096",
"sha256:13f75fb18486759da3ff40f5345d9dd20e7d78f2a39c5884d013456cec9876f0",
"sha256:35b670b0abcfed7cad76f2834041dcf7ae47fd9b22b63622d67cdc933d79f453",
"sha256:4c73f373b0800eb3062ffd13d4a7a2a6d522792fa6eb204d67a4fad0a40f03dc",
"sha256:5759edf0b686b6f25a5d4a447ea588983a33afc8a0081a0954184a4a87fd0dd7",
"sha256:5a7cf6044467c1356b2b49ef69e50bf4d231e773c3ca0558807cdba56b76820b",
"sha256:69c5d920a0b2a9838e677f78f4dde506b95ea8e4d30da25859db6469ded84fa8",
"sha256:8778a5cc5a8437a561e3276b85367412e10ae9fff07db1eed986e427d9a674f8",
"sha256:9871ef5ee17f388f1cb35f76dc6106d40cb8165c562d573470672f4cdefa59ef",
"sha256:9c31d52f1a7dd2bb4681d9f62646c7aa554f19e8e9addc17e8b1b20011d7522d",
"sha256:ab8173a8efe5418bbe50e43f321994ac6673afc5c7c4839014cf6401bbdd0705",
"sha256:ae961f1f0e270f1e4e2273f6a539b2ea33248e0e3a11ffb479d757918a5e03a9",
"sha256:b3c4f93fcb6e97d993bf87cdd917883b7dab7d20c627699f360a8fb49e9e0b91",
"sha256:c9410ce8a3dee77653bc0684cfa1535a7f9c291663bd7ad79e39f5ab58f67ab3",
"sha256:f69e0f7b7c09f1f612b1f8f59e2df72faa8a6b41c5a436dde5b615aaf948f107",
"sha256:faa42a78d1350b02a7d2f0dbe3c80791cf785663d6997891549d0f86dc49125e"
],
"index": "pypi",
"version": "==1.0.4"
"version": "==1.0.5"
},
"psycopg2-binary": {
"hashes": [
@ -656,6 +685,13 @@
],
"version": "==0.16.0"
},
"python-box": {
"hashes": [
"sha256:2df0d0e0769b6d6e7daed8d5e0b10a38e0b5486ee75914c30f2a927f7a374111",
"sha256:ddea019b4ee53fe3f822407b0b26ec54ff6233042c68b54244d3503ae4d6218f"
],
"version": "==5.0.1"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
@ -678,6 +714,61 @@
],
"version": "==1.0.4"
},
"python-levenshtein-wheels": {
"hashes": [
"sha256:0065529c8aec4c044468286177761857d36981ba6f7fdb62d7d5f7ffd143de5d",
"sha256:016924a59d689f9f47d5f7b26b70f31e309255e8dd72602c91e93ceb752b9f92",
"sha256:089d046ea7727e583233c71fef1046663ed67b96967063ae8ddc9f551e86a4fc",
"sha256:09f9faaaa8f65726f91b44c11d3d622fee0f1780cfbe2bf3f410dd0e7345adcb",
"sha256:0aea217eab612acd45dcc3424a2e8dbd977cc309f80359d0c01971f1e65b9a9b",
"sha256:0beb91ad80b1573829066e5af36b80190c367be6e0a65292f073353b0388c7fc",
"sha256:0ec1bc73f5ed3a1a06e02d13bb3cd22a0b32ebf65a9667bbccba106bfa0546f1",
"sha256:0fa2ca69ef803bc6037a8c919e2e8a17b55e94c9c9ffcb4c21befbb15a1d0f40",
"sha256:11c77d0d74ab7f46f89a58ae9c2d67349ebc1ae3e18636627f9939d810167c31",
"sha256:19a68716a322486ddffc8bf7e5cf44a82f7700b05a10658e6e7fc5c7ae92b13d",
"sha256:19a95a01d28d63b042438ba860c4ace90362906a038fa77962ba33325d377d10",
"sha256:1a61f3a51e00a3608659bbaabb3f27af37c9dbe84d843369061a3e45cf0d5103",
"sha256:1c50aebebab403fb2dd415d70355446ac364dece502b0e2737a1a085bb9a4aa4",
"sha256:1d2390d04f9b673391e5ce1a0b054d0565f2e00ea5d1187a044221dc5c02c3e6",
"sha256:1e51cdc123625a28709662d24ea0cb4cf6f991845e6054d9f803c78da1d6b08f",
"sha256:1eca6dc97dfcf588f53281fe48a6d5c423d4e14bdab658a1aa6efd447acc64e0",
"sha256:1f0056d3216b0fe38f25c6f8ebc84bd9f6d34c55a7a9414341b674fb98961399",
"sha256:228b59460e9a786e498bdfc8011838b89c6054650b115c86c9c819a055a793b0",
"sha256:23020f9ff2cb3457a926dcc470b84f9bd5b7646bd8b8e06b915bdbbc905cb23f",
"sha256:2b7b7cf0f43b677f818aa9a610464abf06106c19a51b9ac35bd051a439f337a5",
"sha256:3b591c9a7e91480f0d7bf2041d325f578b9b9c2f2d593304377cb28862e7f9a2",
"sha256:3ca9c70411ab587d071c1d8fc8b69d0558be8e4aa920f2595e2cb5eb229ccc4c",
"sha256:3e6bcca97a7ff4e720352b57ddc26380c0583dcdd4b791acef7b574ad58468a7",
"sha256:3ed88f9e638da57647149115c34e0e120cae6f3d35eee7d77e22cc9c1d8eced3",
"sha256:445bf7941cb1fa05d6c2a4a502ad4868a5cacd92e8eb77b2bd008cdda9d37c55",
"sha256:4ba5e147d76d7ee884fd6eae461438b080bcc9f2c6eb9b576811e1bcfe8f808e",
"sha256:4bb128b719c30f3b9feacfe71a338ae07d39dbffc077139416f3535c89f12362",
"sha256:4e951907b9b5d40c9f1b611c8bdfe46ff8cf8371877cebbd589bf5840feab662",
"sha256:53c0c9964390368fd64460b690f168221c669766b193b7e80ae3950c2b9551f8",
"sha256:57c4edef81611098d37176278f2b6a3712bf864eed313496d7d80504805896d1",
"sha256:5b36e406937c6463d1c1ef3dd82d3f771d9d845f21351e8a026fe4dd398ea8d0",
"sha256:7d0821dab24b430dfdc2cba70a06e6d7a45cb839d0dd0e6db97bb99e23c3d884",
"sha256:7f7283dfe50eac8a8cd9b777de9eb50b1edf7dbb46fc7cc9d9b0050d0c135021",
"sha256:7f9759095b3fc825464a72b1cae95125e610eba3c70f91557754c32a0bf32ea2",
"sha256:8005a4df455569c0d490ddfd9e5a163f21293477fd0ed4ea9effdd723ddd8eaa",
"sha256:86e865f29ad3dc3bb4733e5247220173d90f05ac8d2ad18e9689a220f90de55f",
"sha256:98727050ba70eb8d318ec8a8203531c20119347fc8f281102b097326812742ab",
"sha256:ac9cdf044dcb9481c7da782db01b50c1f0e7cdd78c8507b963b6d072829c0263",
"sha256:acfad8ffed96891fe7c583d92717cd8ec0c03b59a954c389fd4e26a5cdeac610",
"sha256:ad15f25abff8220e556d64e2a27c646241b08f00faf1bc02313655696cd3edfa",
"sha256:b679f951f842c38665aa54bea4d7403099131f71fac6d8584f893a731fe1266d",
"sha256:b8c183dc4aa4e95dc5c373eedc3d205c176805835611fcfec5d9050736c695c4",
"sha256:c097a6829967c76526a037ed34500a028f78f0d765c8e3dbd1a7717afd09fb92",
"sha256:c2c76f483d05eddec60a5cd89e92385adef565a4f243b1d9a6abe2f6bd2a7c0a",
"sha256:c388baa3c04272a7c585d3da24030c142353eb26eb531dd2681502e6be7d7a26",
"sha256:cb0f2a711db665b5bf8697b5af3b9884bb1139385c5c12c2e472e4bbee62da99",
"sha256:cbac984d7b36e75b440d1c8ff9d3425d778364a0cbc23f8943383d4decd35d5e",
"sha256:f55adf069be2d655f8d668594fe1be1b84d9dc8106d380a9ada06f34941c33c8",
"sha256:f9084ed3b8997ad4353d124b903f2860a9695b9e080663276d9e58c32e293244",
"sha256:fb7df3504222fcb1fa593f76623abbb54d6019eec15aac5d05cd07ad90ac016c"
],
"version": "==0.13.1"
},
"pytz": {
"hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
@ -711,11 +802,11 @@
},
"requests": {
"hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"index": "pypi",
"version": "==2.23.0"
"version": "==2.24.0"
},
"sentry-sdk": {
"extras": [
@ -751,11 +842,11 @@
},
"sphinx": {
"hashes": [
"sha256:74fbead182a611ce1444f50218a1c5fc70b6cc547f64948f5182fb30a2a20258",
"sha256:97c9e3bcce2f61d9f5edf131299ee9d1219630598d9f9a8791459a4d9e815be5"
"sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00",
"sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"
],
"index": "pypi",
"version": "==3.1.1"
"version": "==3.1.2"
},
"sphinxcontrib-applehelp": {
"hashes": [
@ -802,49 +893,48 @@
"spiffworkflow": {
"editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "b8a064a0bb76c705a1be04ee9bb8ac7beee56eb0"
"ref": "74529738b4e16be5aadd846669a201560f81a6d4"
},
"sqlalchemy": {
"hashes": [
"sha256:128bc917ed20d78143a45024455ff0aed7d3b96772eba13d5dbaf9cc57e5c41b",
"sha256:156a27548ba4e1fed944ff9fcdc150633e61d350d673ae7baaf6c25c04ac1f71",
"sha256:27e2efc8f77661c9af2681755974205e7462f1ae126f498f4fe12a8b24761d15",
"sha256:2a12f8be25b9ea3d1d5b165202181f2b7da4b3395289000284e5bb86154ce87c",
"sha256:31c043d5211aa0e0773821fcc318eb5cbe2ec916dfbc4c6eea0c5188971988eb",
"sha256:65eb3b03229f684af0cf0ad3bcc771970c1260a82a791a8d07bffb63d8c95bcc",
"sha256:6cd157ce74a911325e164441ff2d9b4e244659a25b3146310518d83202f15f7a",
"sha256:703c002277f0fbc3c04d0ae4989a174753a7554b2963c584ce2ec0cddcf2bc53",
"sha256:869bbb637de58ab0a912b7f20e9192132f9fbc47fc6b5111cd1e0f6cdf5cf9b0",
"sha256:8a0e0cd21da047ea10267c37caf12add400a92f0620c8bc09e4a6531a765d6d7",
"sha256:8d01e949a5d22e5c4800d59b50617c56125fc187fbeb8fa423e99858546de616",
"sha256:925b4fe5e7c03ed76912b75a9a41dfd682d59c0be43bce88d3b27f7f5ba028fb",
"sha256:9cb1819008f0225a7c066cac8bb0cf90847b2c4a6eb9ebb7431dbd00c56c06c5",
"sha256:a87d496884f40c94c85a647c385f4fd5887941d2609f71043e2b73f2436d9c65",
"sha256:a9030cd30caf848a13a192c5e45367e3c6f363726569a56e75dc1151ee26d859",
"sha256:a9e75e49a0f1583eee0ce93270232b8e7bb4b1edc89cc70b07600d525aef4f43",
"sha256:b50f45d0e82b4562f59f0e0ca511f65e412f2a97d790eea5f60e34e5f1aabc9a",
"sha256:b7878e59ec31f12d54b3797689402ee3b5cfcb5598f2ebf26491732758751908",
"sha256:ce1ddaadee913543ff0154021d31b134551f63428065168e756d90bdc4c686f5",
"sha256:ce2646e4c0807f3461be0653502bb48c6e91a5171d6e450367082c79e12868bf",
"sha256:ce6c3d18b2a8ce364013d47b9cad71db815df31d55918403f8db7d890c9d07ae",
"sha256:e4e2664232005bd306f878b0f167a31f944a07c4de0152c444f8c61bbe3cfb38",
"sha256:e8aa395482728de8bdcca9cc0faf3765ab483e81e01923aaa736b42f0294f570",
"sha256:eb4fcf7105bf071c71068c6eee47499ab8d4b8f5a11fc35147c934f0faa60f23",
"sha256:ed375a79f06cad285166e5be74745df1ed6845c5624aafadec4b7a29c25866ef",
"sha256:f35248f7e0d63b234a109dd72fbfb4b5cb6cb6840b221d0df0ecbf54ab087654",
"sha256:f502ef245c492b391e0e23e94cba030ab91722dcc56963c85bfd7f3441ea2bbe",
"sha256:fe01bac7226499aedf472c62fa3b85b2c619365f3f14dd222ffe4f3aa91e5f98"
"sha256:0942a3a0df3f6131580eddd26d99071b48cfe5aaf3eab2783076fbc5a1c1882e",
"sha256:0ec575db1b54909750332c2e335c2bb11257883914a03bc5a3306a4488ecc772",
"sha256:109581ccc8915001e8037b73c29590e78ce74be49ca0a3630a23831f9e3ed6c7",
"sha256:16593fd748944726540cd20f7e83afec816c2ac96b082e26ae226e8f7e9688cf",
"sha256:427273b08efc16a85aa2b39892817e78e3ed074fcb89b2a51c4979bae7e7ba98",
"sha256:50c4ee32f0e1581828843267d8de35c3298e86ceecd5e9017dc45788be70a864",
"sha256:512a85c3c8c3995cc91af3e90f38f460da5d3cade8dc3a229c8e0879037547c9",
"sha256:57aa843b783179ab72e863512e14bdcba186641daf69e4e3a5761d705dcc35b1",
"sha256:621f58cd921cd71ba6215c42954ffaa8a918eecd8c535d97befa1a8acad986dd",
"sha256:6ac2558631a81b85e7fb7a44e5035347938b0a73f5fdc27a8566777d0792a6a4",
"sha256:716754d0b5490bdcf68e1e4925edc02ac07209883314ad01a137642ddb2056f1",
"sha256:736d41cfebedecc6f159fc4ac0769dc89528a989471dc1d378ba07d29a60ba1c",
"sha256:8619b86cb68b185a778635be5b3e6018623c0761dde4df2f112896424aa27bd8",
"sha256:87fad64529cde4f1914a5b9c383628e1a8f9e3930304c09cf22c2ae118a1280e",
"sha256:89494df7f93b1836cae210c42864b292f9b31eeabca4810193761990dc689cce",
"sha256:8cac7bb373a5f1423e28de3fd5fc8063b9c8ffe8957dc1b1a59cb90453db6da1",
"sha256:8fd452dc3d49b3cc54483e033de6c006c304432e6f84b74d7b2c68afa2569ae5",
"sha256:adad60eea2c4c2a1875eb6305a0b6e61a83163f8e233586a4d6a55221ef984fe",
"sha256:c26f95e7609b821b5f08a72dab929baa0d685406b953efd7c89423a511d5c413",
"sha256:cbe1324ef52ff26ccde2cb84b8593c8bf930069dfc06c1e616f1bfd4e47f48a3",
"sha256:d05c4adae06bd0c7f696ae3ec8d993ed8ffcc4e11a76b1b35a5af8a099bd2284",
"sha256:d98bc827a1293ae767c8f2f18be3bb5151fd37ddcd7da2a5f9581baeeb7a3fa1",
"sha256:da2fb75f64792c1fc64c82313a00c728a7c301efe6a60b7a9fe35b16b4368ce7",
"sha256:e4624d7edb2576cd72bb83636cd71c8ce544d8e272f308bd80885056972ca299",
"sha256:e89e0d9e106f8a9180a4ca92a6adde60c58b1b0299e1b43bd5e0312f535fbf33",
"sha256:f11c2437fb5f812d020932119ba02d9e2bc29a6eca01a055233a8b449e3e1e7d",
"sha256:f57be5673e12763dd400fea568608700a63ce1c6bd5bdbc3cc3a2c5fdb045274",
"sha256:fc728ece3d5c772c196fd338a99798e7efac7a04f9cb6416299a3638ee9a94cd"
],
"version": "==1.3.17"
"version": "==1.3.18"
},
"swagger-ui-bundle": {
"hashes": [
"sha256:49d2e12d60a6499e9d37ea37953b5d700f4e114edc7520fe918bae5eb693a20e",
"sha256:c5373b683487b1b914dccd23bcd9a3016afa2c2d1cda10f8713c0a9af0f91dd3",
"sha256:f776811855092c086dbb08216c8810a84accef8c76c796a135caa13645c5cc68"
"sha256:f5255f786cde67a2638111f4a7d04355836743198a83c4ecbe815d9fc384b0c8",
"sha256:f5691167f2e9f73ecbe8229a89454ae5ea958f90bb0d4583ed7adaae598c4122"
],
"index": "pypi",
"version": "==0.0.6"
"version": "==0.0.8"
},
"urllib3": {
"hashes": [
@ -890,6 +980,13 @@
"index": "pypi",
"version": "==1.0.1"
},
"wtforms": {
"hashes": [
"sha256:6ff8635f4caeed9f38641d48cfe019d0d3896f41910ab04494143fc027866e1b",
"sha256:861a13b3ae521d6700dac3b2771970bd354a63ba7043ecc3a82b5288596a1972"
],
"version": "==2.3.1"
},
"xlrd": {
"hashes": [
"sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2",
@ -924,48 +1021,51 @@
},
"coverage": {
"hashes": [
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
"sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d",
"sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2",
"sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703",
"sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404",
"sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7",
"sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405",
"sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d",
"sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c",
"sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6",
"sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70",
"sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40",
"sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4",
"sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613",
"sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10",
"sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b",
"sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0",
"sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec",
"sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1",
"sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d",
"sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913",
"sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e",
"sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62",
"sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e",
"sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a",
"sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d",
"sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f",
"sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e",
"sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b",
"sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c",
"sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032",
"sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a",
"sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee",
"sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c",
"sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"
],
"index": "pypi",
"version": "==5.1"
"version": "==5.2"
},
"importlib-metadata": {
"hashes": [
"sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545",
"sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
],
"markers": "python_version < '3.8'",
"version": "==1.6.1"
"version": "==1.7.0"
},
"more-itertools": {
"hashes": [
@ -998,10 +1098,10 @@
},
"py": {
"hashes": [
"sha256:a673fa23d7000440cc885c17dbd34fafcb7d7a6e230b29f6766400de36a33c44",
"sha256:f3b3a4c36512a4c4f024041ab51866f11761cc169670204b235f6b20523d4e6b"
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
],
"version": "==1.8.2"
"version": "==1.9.0"
},
"pyparsing": {
"hashes": [
@ -1027,10 +1127,10 @@
},
"wcwidth": {
"hashes": [
"sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f",
"sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f"
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.4"
"version": "==0.2.5"
},
"zipp": {
"hashes": [

View File

@ -1,4 +1,4 @@
# CrConnectFrontend
# sartography/cr-connect-workflow
[![Build Status](https://travis-ci.com/sartography/cr-connect-workflow.svg?branch=master)](https://travis-ci.com/sartography/cr-connect-workflow)
@ -27,7 +27,7 @@ Make sure all of the following are properly installed on your system:
- Select the directory where you cloned this repository and click `Ok`.
- Expand the `Project Interpreter` section.
- Select the `New environment using` radio button and choose `Pipenv` in the dropdown.
- Under `Base interpreter`, select `Python 3.6`
- Under `Base interpreter`, select `Python 3.7`
- In the `Pipenv executable` field, enter `/home/your_username_goes_here/.local/bin/pipenv`
- Click `Create`
![Project Interpreter](readme_images/new_project.png)
@ -47,22 +47,15 @@ run configuration so it doesn't go away.) :
Just click the "Play" button next to RUN in the top right corner of the screen.
The Swagger based view of the API will be avialable at http://0.0.0.0:5000/v1.0/ui/
### Testing from the Shell
This app includes a command line interface that will read in BPMN files and let you
play with it at the command line. To run it right click on app/command_line/joke.py and
click run. Type "?" to get a list of commands.
So far the joke system will work a little, when you file it up try these commands
in this order:
```bash
> engine (this will run all tasks up to first user task and should print a joke)
> answer clock (this is the correct answer)
> next (this completes the user task)
> engine (this runs the rest of the tasks, and should tell you that you got the question right)
### Running Tests
We use pytest to execute tests. You can run this from the command line with:
```
pipenv run coverage run -m pytest
```
To run the tests within PyCharm set up a run configuration using pytest (Go to Run, configurations, click the
plus icon, select Python Tests, and under this select pytest, defaults should work good-a-plenty with no
additional edits required.)
You can try re-running this and getting the question wrong.
You might open up the Joke bpmn diagram so you can see what this looks like to
draw out.
## Documentation
Additional Documentation is available on [ReadTheDocs](https://cr-connect-workflow.readthedocs.io/en/latest/#)

View File

@ -15,7 +15,8 @@ TEST_UID = environ.get('TEST_UID', default="dhf8r")
ADMIN_UIDS = re.split(r',\s*', environ.get('ADMIN_UIDS', default="dhf8r,ajl2j,cah3us,cl3wf"))
# Sentry flag
ENABLE_SENTRY = environ.get('ENABLE_SENTRY', default="false") == "true"
ENABLE_SENTRY = environ.get('ENABLE_SENTRY', default="false") == "true" # To be removed soon
SENTRY_ENVIRONMENT = environ.get('SENTRY_ENVIRONMENT', None)
# Add trailing slash to base path
APPLICATION_ROOT = re.sub(r'//', '/', '/%s/' % environ.get('APPLICATION_ROOT', default="/").strip('/'))
@ -30,7 +31,7 @@ SQLALCHEMY_DATABASE_URI = environ.get(
default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME)
)
TOKEN_AUTH_TTL_HOURS = float(environ.get('TOKEN_AUTH_TTL_HOURS', default=24))
TOKEN_AUTH_SECRET_KEY = environ.get('TOKEN_AUTH_SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.")
SECRET_KEY = environ.get('SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.")
FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session")
SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER")
@ -46,6 +47,7 @@ LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu").strip('/') # No
LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=1))
# Email configuration
DEFAULT_SENDER = 'askresearch@virginia.edu'
FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com']
MAIL_DEBUG = environ.get('MAIL_DEBUG', default=True)
MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io')

View File

@ -5,7 +5,7 @@ basedir = os.path.abspath(os.path.dirname(__file__))
NAME = "CR Connect Workflow"
TESTING = True
TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod."
SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod."
# This is here, for when we are running the E2E Tests in the frontend code bases.
# which will set the TESTING envronment to true, causing this to execute, but we need

View File

@ -4,6 +4,8 @@ import sentry_sdk
import connexion
from jinja2 import Environment, FileSystemLoader
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_cors import CORS
from flask_marshmallow import Marshmallow
from flask_mail import Mail
@ -32,30 +34,31 @@ db = SQLAlchemy(app)
session = db.session
""":type: sqlalchemy.orm.Session"""
# Mail settings
mail = Mail(app)
migrate = Migrate(app, db)
ma = Marshmallow(app)
from crc import models
from crc import api
from crc.api import admin
connexion_app.add_api('api.yml', base_path='/v1.0')
# Convert list of allowed origins to list of regexes
origins_re = [r"^https?:\/\/%s(.*)" % o.replace('.', '\.') for o in app.config['CORS_ALLOW_ORIGINS']]
cors = CORS(connexion_app.app, origins=origins_re)
if app.config['ENABLE_SENTRY']:
# Sentry error handling
if app.config['SENTRY_ENVIRONMENT']:
sentry_sdk.init(
environment=app.config['SENTRY_ENVIRONMENT'],
dsn="https://25342ca4e2d443c6a5c49707d68e9f40@o401361.ingest.sentry.io/5260915",
integrations=[FlaskIntegration()]
)
# Jinja environment definition, used to render mail templates
template_dir = os.getcwd() + '/crc/static/templates/mails'
env = Environment(loader=FileSystemLoader(template_dir))
# Mail settings
mail = Mail(app)
print('=== USING THESE CONFIG SETTINGS: ===')
print('APPLICATION_ROOT = ', app.config['APPLICATION_ROOT'])
print('CORS_ALLOW_ORIGINS = ', app.config['CORS_ALLOW_ORIGINS'])
@ -88,3 +91,4 @@ def clear_db():
"""Load example data into the database."""
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()

View File

@ -502,7 +502,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/File"
# /v1.0/workflow/0
/reference_file:
get:
operationId: crc.api.file.get_reference_files
@ -565,6 +564,26 @@ paths:
type: string
format: binary
example: '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>'
/task_events:
parameters:
- name: action
in: query
required: false
description: The type of action the event documents, options include "ASSIGNMENT" for tasks that are waiting on you, "COMPLETE" for things have completed.
schema:
type: string
get:
operationId: crc.api.workflow.get_task_events
summary: Returns a list of task events related to the current user. Can be filtered by type.
tags:
- Workflows and Tasks
responses:
'200':
description: Returns details about tasks that are waiting on the current user.
content:
application/json:
schema:
$ref: "#/components/schemas/TaskEvent"
# /v1.0/workflow/0
/workflow/{workflow_id}:
parameters:
@ -626,6 +645,12 @@ paths:
schema:
type: string
format: uuid
- name: terminate_loop
in: query
required: false
description: Terminate the loop on a looping task
schema:
type: boolean
put:
operationId: crc.api.workflow.update_task
summary: Exclusively for User Tasks, submits form data as a flat set of key/values.
@ -697,12 +722,19 @@ paths:
description: The string to search for in the Value column of the lookup table.
schema:
type: string
- name: value
in: query
required: false
description: An alternative to query, this accepts the specific value or id selected in a dropdown list or auto-complete, and will return the one matching record. Useful for getting additional details about an item selected in a dropdown.
schema:
type: string
- name: limit
in: query
required: false
description: The total number of records to return, defaults to 10.
schema:
type: integer
get:
operationId: crc.api.workflow.lookup
summary: Provides type-ahead search against a lookup table associted with a form field.
@ -806,6 +838,33 @@ paths:
type: array
items:
$ref: "#/components/schemas/Script"
/eval:
parameters:
- name: expression
in: query
required: true
description: The python expression to execute.
schema:
type: string
put:
operationId: crc.api.tools.evaluate_python_expression
summary: Execute the given python expression, with the given json data structure used as local variables, returns the result of the evaluation.
tags:
- Configurator Tools
requestBody:
description: The json data to use as local variables when evaluating the expresson.
required: true
content:
application/json:
schema:
type: object
responses:
'200':
description: Returns the result of executing the given python script.
content:
text/plain:
schema:
type: string
/approval-counts:
parameters:
- name: as_user
@ -917,6 +976,21 @@ paths:
application/json:
schema:
type: object
/health_attesting:
get:
operationId: crc.api.approval.get_health_attesting_csv
summary: Returns a CSV file with health attesting records
tags:
- Approvals
responses:
'200':
description: A CSV file
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Approval"
components:
securitySchemes:
jwt:
@ -1164,6 +1238,36 @@ components:
value: "model.my_boolean_field_id && model.my_enum_field_value !== 'something'"
- id: "hide_expression"
value: "model.my_enum_field_value === 'something'"
TaskEvent:
properties:
workflow:
$ref: "#/components/schemas/Workflow"
study:
$ref: "#/components/schemas/Study"
workflow_sec:
$ref: "#/components/schemas/WorkflowSpec"
spec_version:
type: string
action:
type: string
task_id:
type: string
task_type:
type: string
task_lane:
type: string
form_data:
type: object
mi_type:
type: string
mi_count:
type: integer
mi_index:
type: integer
process_name:
type: string
date:
type: string
Form:
properties:
key:

72
crc/api/admin.py Normal file
View File

@ -0,0 +1,72 @@
# Admin app
import json
from flask import url_for
from flask_admin import Admin
from flask_admin.contrib import sqla
from flask_admin.contrib.sqla import ModelView
from werkzeug.utils import redirect
from jinja2 import Markup
from crc import db, app
from crc.api.user import verify_token, verify_token_admin
from crc.models.approval import ApprovalModel
from crc.models.file import FileModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel
class AdminModelView(sqla.ModelView):
can_create = False
can_edit = False
can_delete = False
page_size = 50 # the number of entries to display on the list view
column_exclude_list = ['bpmn_workflow_json', ]
column_display_pk = True
can_export = True
def is_accessible(self):
return verify_token_admin()
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
return redirect(url_for('home'))
class UserView(AdminModelView):
column_filters = ['uid']
class StudyView(AdminModelView):
column_filters = ['id', 'primary_investigator_id']
column_searchable_list = ['title']
class ApprovalView(AdminModelView):
column_filters = ['study_id', 'approver_uid']
class WorkflowView(AdminModelView):
column_filters = ['study_id', 'id']
class FileView(AdminModelView):
column_filters = ['workflow_id']
def json_formatter(view, context, model, name):
value = getattr(model, name)
json_value = json.dumps(value, ensure_ascii=False, indent=2)
return Markup('<pre>{}</pre>'.format(json_value))
class TaskEventView(AdminModelView):
column_filters = ['workflow_id', 'action']
column_list = ['study_id', 'user_id', 'workflow_id', 'action', 'task_title', 'form_data', 'date']
column_formatters = {
'form_data': json_formatter,
}
admin = Admin(app)
admin.add_view(StudyView(StudyModel, db.session))
admin.add_view(ApprovalView(ApprovalModel, db.session))
admin.add_view(UserView(UserModel, db.session))
admin.add_view(WorkflowView(WorkflowModel, db.session))
admin.add_view(FileView(FileModel, db.session))
admin.add_view(TaskEventView(TaskEventModel, db.session))

View File

@ -1,9 +1,11 @@
import csv
import io
import json
import pickle
from base64 import b64decode
from datetime import datetime
from flask import g
from flask import g, make_response
from crc import db, session
from crc.api.common import ApiError
@ -88,71 +90,25 @@ def get_approvals_for_study(study_id=None):
return results
def get_health_attesting_csv():
records = ApprovalService.get_health_attesting_records()
si = io.StringIO()
cw = csv.writer(si)
cw.writerows(records)
output = make_response(si.getvalue())
output.headers["Content-Disposition"] = "attachment; filename=health_attesting.csv"
output.headers["Content-type"] = "text/csv"
return output
# ----- Begin descent into madness ---- #
def get_csv():
"""A damn lie, it's a json file. A huge bit of a one-off for RRT, but 3 weeks of midnight work can convince a
man to do just about anything"""
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
output = []
errors = []
for approval in approvals:
try:
if approval.status != ApprovalStatus.APPROVED.value:
continue
for related_approval in approval.related_approvals:
if related_approval.status != ApprovalStatus.APPROVED.value:
continue
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == approval.workflow_id).first()
data = json.loads(workflow.bpmn_workflow_json)
last_task = find_task(data['last_task']['__uuid__'], data['task_tree'])
personnel = extract_value(last_task, 'personnel')
training_val = extract_value(last_task, 'RequiredTraining')
pi_supervisor = extract_value(last_task, 'PISupervisor')['value']
review_complete = 'AllRequiredTraining' in training_val
pi_uid = workflow.study.primary_investigator_id
pi_details = LdapService.user_info(pi_uid)
details = []
details.append(pi_details)
for person in personnel:
uid = person['PersonnelComputingID']['value']
details.append(LdapService.user_info(uid))
content = ApprovalService.get_not_really_csv_content()
for person in details:
record = {
"study_id": approval.study_id,
"pi_uid": pi_details.uid,
"pi": pi_details.display_name,
"name": person.display_name,
"uid": person.uid,
"email": person.email_address,
"supervisor": "",
"review_complete": review_complete,
}
# We only know the PI's supervisor.
if person.uid == pi_details.uid:
record["supervisor"] = pi_supervisor
return content
output.append(record)
except Exception as e:
errors.append("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
return {"results": output, "errors": errors }
def extract_value(task, key):
if key in task['data']:
return pickle.loads(b64decode(task['data'][key]['__bytes__']))
else:
return ""
def find_task(uuid, task):
if task['id']['__uuid__'] == uuid:
return task
for child in task['children']:
task = find_task(uuid, child)
if task:
return task
# ----- come back to the world of the living ---- #

View File

@ -25,6 +25,7 @@ class ApiError(Exception):
instance.task_name = task.task_spec.description or ""
instance.file_name = task.workflow.spec.file or ""
instance.task_data = task.data
app.logger.error(message, exc_info=True)
return instance
@classmethod
@ -35,6 +36,7 @@ class ApiError(Exception):
instance.task_name = task_spec.description or ""
if task_spec._wf_spec:
instance.file_name = task_spec._wf_spec.file
app.logger.error(message, exc_info=True)
return instance
@classmethod

View File

@ -2,6 +2,7 @@ import io
import json
import connexion
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from flask import send_file
from jinja2 import Template, UndefinedError
@ -10,11 +11,12 @@ from crc.scripts.complete_template import CompleteTemplate
from crc.scripts.script import Script
import crc.scripts
from crc.services.mails import send_test_email
from crc.services.workflow_processor import WorkflowProcessor
def render_markdown(data, template):
"""
Provides a quick way to very that a Jinja markdown template will work properly on a given json
Provides a quick way to very that a Jinja markdown template will work properly on a given json
data structure. Useful for folks that are building these markdown templates.
"""
try:
@ -61,8 +63,22 @@ def list_scripts():
})
return script_meta
def send_email(address):
"""Just sends a quick test email to assure the system is working."""
if not address:
address = "dan@sartography.com"
return send_test_email(address, [address])
return send_test_email(address, [address])
def evaluate_python_expression(expression, body):
"""Evaluate the given python expression, returning it's result. This is useful if the
front end application needs to do real-time processing on task data. If for instance
there is a hide expression that is based on a previous value in the same form."""
try:
# fixme: The script engine should be pulled from Workflow Processor,
# but the one it returns overwrites the evaluate expression making it uncallable.
script_engine = PythonScriptEngine()
return script_engine.evaluate(expression, **body)
except Exception as e:
raise ApiError("expression_error", str(e))

View File

@ -1,12 +1,13 @@
import uuid
from SpiffWorkflow.util.deep_merge import DeepMerge
from flask import g
from crc import session, app
from crc.api.common import ApiError, ApiErrorSchema
from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema
from crc.models.file import FileModel, LookupDataSchema
from crc.models.stats import TaskEventModel
from crc.models.study import StudyModel, WorkflowMetadata
from crc.models.task_event import TaskEventModel, TaskEventModelSchema, TaskEvent, TaskEventSchema
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \
WorkflowSpecCategoryModelSchema
from crc.services.file_service import FileService
@ -41,7 +42,6 @@ def get_workflow_specification(spec_id):
def validate_workflow_specification(spec_id):
errors = []
try:
WorkflowService.test_spec(spec_id)
@ -57,7 +57,6 @@ def validate_workflow_specification(spec_id):
return ApiErrorSchema(many=True).dump(errors)
def update_workflow_specification(spec_id, body):
if spec_id is None:
raise ApiError('unknown_spec', 'Please provide a valid Workflow Spec ID.')
@ -89,115 +88,95 @@ def delete_workflow_specification(spec_id):
session.query(TaskEventModel).filter(TaskEventModel.workflow_spec_id == spec_id).delete()
# Delete all stats and workflow models related to this specification
# Delete all events and workflow models related to this specification
for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id):
StudyService.delete_workflow(workflow)
session.query(WorkflowSpecModel).filter_by(id=spec_id).delete()
session.commit()
def __get_workflow_api_model(processor: WorkflowProcessor, next_task = None):
"""Returns an API model representing the state of the current workflow, if requested, and
possible, next_task is set to the current_task."""
nav_dict = processor.bpmn_workflow.get_nav_list()
navigation = []
for nav_item in nav_dict:
spiff_task = processor.bpmn_workflow.get_task(nav_item['task_id'])
if 'description' in nav_item:
nav_item['title'] = nav_item.pop('description')
# fixme: duplicate code from the workflow_service. Should only do this in one place.
if ' ' in nav_item['title']:
nav_item['title'] = nav_item['title'].partition(' ')[2]
else:
nav_item['title'] = ""
if spiff_task:
nav_item['task'] = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
nav_item['title'] = nav_item['task'].title # Prefer the task title.
else:
nav_item['task'] = None
if not 'is_decision' in nav_item:
nav_item['is_decision'] = False
navigation.append(NavigationItem(**nav_item))
NavigationItemSchema().dump(nav_item)
spec = session.query(WorkflowSpecModel).filter_by(id=processor.workflow_spec_id).first()
workflow_api = WorkflowApi(
id=processor.get_workflow_id(),
status=processor.get_status(),
next_task=None,
navigation=navigation,
workflow_spec_id=processor.workflow_spec_id,
spec_version=processor.get_version_string(),
is_latest_spec=processor.is_latest_spec,
total_tasks=len(navigation),
completed_tasks=processor.workflow_model.completed_tasks,
last_updated=processor.workflow_model.last_updated,
title=spec.display_name
)
if not next_task: # The Next Task can be requested to be a certain task, useful for parallel tasks.
# This may or may not work, sometimes there is no next task to complete.
next_task = processor.next_task()
if next_task:
workflow_api.next_task = WorkflowService.spiff_task_to_api_task(next_task, add_docs_and_forms=True)
return workflow_api
def get_workflow(workflow_id, soft_reset=False, hard_reset=False):
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset)
workflow_api_model = __get_workflow_api_model(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
WorkflowService.update_task_assignments(processor)
return WorkflowApiSchema().dump(workflow_api_model)
def get_task_events(action):
"""Provides a way to see a history of what has happened, or get a list of tasks that need your attention."""
query = session.query(TaskEventModel).filter(TaskEventModel.user_uid == g.user.uid)
if action:
query = query.filter(TaskEventModel.action == action)
events = query.all()
# Turn the database records into something a little richer for the UI to use.
task_events = []
for event in events:
study = session.query(StudyModel).filter(StudyModel.id == event.study_id).first()
workflow = session.query(WorkflowModel).filter(WorkflowModel.id == event.workflow_id).first()
workflow_meta = WorkflowMetadata.from_workflow(workflow)
task_events.append(TaskEvent(event, study, workflow_meta))
return TaskEventSchema(many=True).dump(task_events)
def delete_workflow(workflow_id):
StudyService.delete_workflow(workflow_id)
def set_current_task(workflow_id, task_id):
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
user_uid = __get_user_uid(workflow_model.study.user_uid)
processor = WorkflowProcessor(workflow_model)
task_id = uuid.UUID(task_id)
task = processor.bpmn_workflow.get_task(task_id)
if task.state != task.COMPLETED and task.state != task.READY:
spiff_task = processor.bpmn_workflow.get_task(task_id)
_verify_user_and_role(processor, spiff_task)
user_uid = g.user.uid
if spiff_task.state != spiff_task.COMPLETED and spiff_task.state != spiff_task.READY:
raise ApiError("invalid_state", "You may not move the token to a task who's state is not "
"currently set to COMPLETE or READY.")
# Only reset the token if the task doesn't already have it.
if task.state == task.COMPLETED:
task.reset_token(reset_data=False) # we could optionally clear the previous data.
if spiff_task.state == spiff_task.COMPLETED:
spiff_task.reset_token(reset_data=True) # Don't try to copy the existing data back into this task.
processor.save()
WorkflowService.log_task_action(user_uid, processor, task, WorkflowService.TASK_ACTION_TOKEN_RESET)
workflow_api_model = __get_workflow_api_model(processor, task)
WorkflowService.log_task_action(user_uid, processor, spiff_task, WorkflowService.TASK_ACTION_TOKEN_RESET)
WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor, spiff_task)
return WorkflowApiSchema().dump(workflow_api_model)
def update_task(workflow_id, task_id, body):
def update_task(workflow_id, task_id, body, terminate_loop=None):
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
if workflow_model is None:
raise ApiError("invalid_workflow_id", "The given workflow id is not valid.", status_code=404)
elif workflow_model.study is None:
raise ApiError("invalid_study", "There is no study associated with the given workflow.", status_code=404)
user_uid = __get_user_uid(workflow_model.study.user_uid)
processor = WorkflowProcessor(workflow_model)
task_id = uuid.UUID(task_id)
task = processor.bpmn_workflow.get_task(task_id)
if task.state != task.READY:
spiff_task = processor.bpmn_workflow.get_task(task_id)
_verify_user_and_role(processor, spiff_task)
if not spiff_task:
raise ApiError("empty_task", "Processor failed to obtain task.", status_code=404)
if spiff_task.state != spiff_task.READY:
raise ApiError("invalid_state", "You may not update a task unless it is in the READY state. "
"Consider calling a token reset to make this task Ready.")
task.update_data(body)
processor.complete_task(task)
if terminate_loop:
spiff_task.terminate_loop()
spiff_task.update_data(body)
processor.complete_task(spiff_task)
processor.do_engine_steps()
processor.save()
WorkflowService.log_task_action(user_uid, processor, task, WorkflowService.TASK_ACTION_COMPLETE)
workflow_api_model = __get_workflow_api_model(processor)
# Log the action, and any pending task assignments in the event of lanes in the workflow.
WorkflowService.log_task_action(g.user.uid, processor, spiff_task, WorkflowService.TASK_ACTION_COMPLETE)
WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
return WorkflowApiSchema().dump(workflow_api_model)
@ -240,7 +219,7 @@ def delete_workflow_spec_category(cat_id):
session.commit()
def lookup(workflow_id, field_id, query, limit):
def lookup(workflow_id, field_id, query=None, value=None, limit=10):
"""
given a field in a task, attempts to find the lookup table or function associated
with that field and runs a full-text query against it to locate the values and
@ -248,16 +227,25 @@ def lookup(workflow_id, field_id, query, limit):
Tries to be fast, but first runs will be very slow.
"""
workflow = session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
lookup_data = LookupService.lookup(workflow, field_id, query, limit)
lookup_data = LookupService.lookup(workflow, field_id, query, value, limit)
return LookupDataSchema(many=True).dump(lookup_data)
def __get_user_uid(user_uid):
if 'user' in g:
if g.user.uid not in app.config['ADMIN_UIDS'] and user_uid != g.user.uid:
raise ApiError("permission_denied", "You are not authorized to edit the task data for this workflow.", status_code=403)
else:
return g.user.uid
def _verify_user_and_role(processor, spiff_task):
"""Assures the currently logged in user can access the given workflow and task, or
raises an error.
Allow administrators to modify tasks, otherwise assure that the current user
is allowed to edit or update the task. Will raise the appropriate error if user
is not authorized. """
else:
if 'user' not in g:
raise ApiError("logged_out", "You are no longer logged in.", status_code=401)
if g.user.uid in app.config['ADMIN_UIDS']:
return g.user.uid
allowed_users = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if g.user.uid not in allowed_users:
raise ApiError.from_task("permission_denied",
f"This task must be completed by '{allowed_users}', "
f"but you are {g.user.uid}", spiff_task)

View File

@ -29,20 +29,44 @@ class NavigationItem(object):
self.state = state
self.is_decision = is_decision
self.task = task
self.lane = lane
class Task(object):
##########################################################################
# Custom properties and validations defined in Camunda form fields #
##########################################################################
# Repeating form section
PROP_OPTIONS_REPEAT = "repeat"
PROP_OPTIONS_FILE = "spreadsheet.name"
PROP_OPTIONS_VALUE_COLUMN = "spreadsheet.value.column"
PROP_OPTIONS_LABEL_COL = "spreadsheet.label.column"
# Read-only field
PROP_OPTIONS_READ_ONLY = "read_only"
# LDAP lookup
PROP_LDAP_LOOKUP = "ldap.lookup"
VALIDATION_REQUIRED = "required"
# Autocomplete field
FIELD_TYPE_AUTO_COMPLETE = "autocomplete"
# Required field
VALIDATION_REQUIRED = "required"
def __init__(self, id, name, title, type, state, form, documentation, data,
multi_instance_type, multi_instance_count, multi_instance_index, process_name, properties):
# Enum field options values pulled from a spreadsheet
PROP_OPTIONS_FILE_NAME = "spreadsheet.name"
PROP_OPTIONS_FILE_VALUE_COLUMN = "spreadsheet.value.column"
PROP_OPTIONS_FILE_LABEL_COLUMN = "spreadsheet.label.column"
# Enum field options values pulled from task data
PROP_OPTIONS_DATA_NAME = "data.name"
PROP_OPTIONS_DATA_VALUE_COLUMN = "data.value.column"
PROP_OPTIONS_DATA_LABEL_COLUMN = "data.label.column"
##########################################################################
def __init__(self, id, name, title, type, state, lane, form, documentation, data,
multi_instance_type, multi_instance_count, multi_instance_index,
process_name, properties):
self.id = id
self.name = name
self.title = title
@ -51,6 +75,7 @@ class Task(object):
self.form = form
self.documentation = documentation
self.data = data
self.lane = lane
self.multi_instance_type = multi_instance_type # Some tasks have a repeat behavior.
self.multi_instance_count = multi_instance_count # This is the number of times the task could repeat.
self.multi_instance_index = multi_instance_index # And the index of the currently repeating task.
@ -60,7 +85,7 @@ class Task(object):
class OptionSchema(ma.Schema):
class Meta:
fields = ["id", "name"]
fields = ["id", "name", "data"]
class ValidationSchema(ma.Schema):
@ -70,15 +95,11 @@ class ValidationSchema(ma.Schema):
class FormFieldPropertySchema(ma.Schema):
class Meta:
fields = [
"id", "value"
]
fields = ["id", "value"]
class FormFieldSchema(ma.Schema):
class Meta:
fields = [
"id", "type", "label", "default_value", "options", "validation", "properties", "value"
]
fields = ["id", "type", "label", "default_value", "options", "validation", "properties", "value"]
default_value = marshmallow.fields.String(required=False, allow_none=True)
options = marshmallow.fields.List(marshmallow.fields.Nested(OptionSchema))
@ -93,7 +114,7 @@ class FormSchema(ma.Schema):
class TaskSchema(ma.Schema):
class Meta:
fields = ["id", "name", "title", "type", "state", "form", "documentation", "data", "multi_instance_type",
fields = ["id", "name", "title", "type", "state", "lane", "form", "documentation", "data", "multi_instance_type",
"multi_instance_count", "multi_instance_index", "process_name", "properties"]
multi_instance_type = EnumField(MultiInstanceType)
@ -101,6 +122,7 @@ class TaskSchema(ma.Schema):
form = marshmallow.fields.Nested(FormSchema, required=False, allow_none=True)
title = marshmallow.fields.String(required=False, allow_none=True)
process_name = marshmallow.fields.String(required=False, allow_none=True)
lane = marshmallow.fields.String(required=False, allow_none=True)
@marshmallow.post_load
def make_task(self, data, **kwargs):
@ -110,10 +132,11 @@ class TaskSchema(ma.Schema):
class NavigationItemSchema(ma.Schema):
class Meta:
fields = ["id", "task_id", "name", "title", "backtracks", "level", "indent", "child_count", "state",
"is_decision", "task"]
"is_decision", "task", "lane"]
unknown = INCLUDE
task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False, allow_none=True)
backtracks = marshmallow.fields.String(required=False, allow_none=True)
lane = marshmallow.fields.String(required=False, allow_none=True)
title = marshmallow.fields.String(required=False, allow_none=True)
task_id = marshmallow.fields.String(required=False, allow_none=True)

View File

@ -57,28 +57,16 @@ class Approval(object):
@classmethod
def from_model(cls, model: ApprovalModel):
# TODO: Reduce the code by iterating over model's dict keys
instance = cls()
instance.id = model.id
instance.study_id = model.study_id
instance.workflow_id = model.workflow_id
instance.version = model.version
instance.approver_uid = model.approver_uid
instance.status = model.status
instance.message = model.message
instance.date_created = model.date_created
instance.date_approved = model.date_approved
instance.version = model.version
instance.title = ''
args = dict((k, v) for k, v in model.__dict__.items() if not k.startswith('_'))
instance = cls(**args)
instance.related_approvals = []
instance.title = model.study.title if model.study else ''
if model.study:
instance.title = model.study.title
try:
instance.approver = LdapService.user_info(model.approver_uid)
instance.primary_investigator = LdapService.user_info(model.study.primary_investigator_id)
except ApiError as ae:
app.logger.error("Ldap lookup failed for approval record %i" % model.id)
app.logger.error(f'Ldap lookup failed for approval record {model.id}', exc_info=True)
doc_dictionary = FileService.get_doc_dictionary()
instance.associated_files = []

18
crc/models/email.py Normal file
View File

@ -0,0 +1,18 @@
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
from marshmallow import EXCLUDE
from sqlalchemy import func
from crc import db
from crc.models.study import StudyModel
class EmailModel(db.Model):
__tablename__ = 'email'
id = db.Column(db.Integer, primary_key=True)
subject = db.Column(db.String)
sender = db.Column(db.String)
recipients = db.Column(db.String)
content = db.Column(db.String)
content_html = db.Column(db.String)
study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=True)
study = db.relationship(StudyModel)

View File

@ -144,7 +144,6 @@ class LookupFileModel(db.Model):
"""Gives us a quick way to tell what kind of lookup is set on a form field.
Connected to the file data model, so that if a new version of the same file is
created, we can update the listing."""
#fixme: What happens if they change the file associated with a lookup field?
__tablename__ = 'lookup_file'
id = db.Column(db.Integer, primary_key=True)
workflow_spec_id = db.Column(db.String)
@ -153,6 +152,7 @@ class LookupFileModel(db.Model):
file_data_model_id = db.Column(db.Integer, db.ForeignKey('file_data.id'))
dependencies = db.relationship("LookupDataModel", lazy="select", backref="lookup_file_model", cascade="all, delete, delete-orphan")
class LookupDataModel(db.Model):
__tablename__ = 'lookup_data'
id = db.Column(db.Integer, primary_key=True)
@ -181,6 +181,7 @@ class LookupDataSchema(SQLAlchemyAutoSchema):
load_instance = True
include_relationships = False
include_fk = False # Includes foreign keys
exclude = ['id'] # Do not include the id field, it should never be used via the API.
class SimpleFileSchema(ma.Schema):

View File

@ -29,6 +29,9 @@ class LdapModel(db.Model):
affiliation=", ".join(entry.uvaPersonIAMAffiliation),
sponsor_type=", ".join(entry.uvaPersonSponsoredType))
def proper_name(self):
return f'{self.display_name} - ({self.uid})'
class LdapSchema(SQLAlchemyAutoSchema):
class Meta:

View File

@ -1,32 +0,0 @@
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from crc import db
class TaskEventModel(db.Model):
__tablename__ = 'task_event'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=False)
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=False)
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False)
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
spec_version = db.Column(db.String)
action = db.Column(db.String)
task_id = db.Column(db.String)
task_name = db.Column(db.String)
task_title = db.Column(db.String)
task_type = db.Column(db.String)
task_state = db.Column(db.String)
mi_type = db.Column(db.String)
mi_count = db.Column(db.Integer)
mi_index = db.Column(db.Integer)
process_name = db.Column(db.String)
date = db.Column(db.DateTime)
class TaskEventModelSchema(SQLAlchemyAutoSchema):
class Meta:
model = TaskEventModel
load_instance = True
include_relationships = True
include_fk = True # Includes foreign keys

View File

@ -31,10 +31,8 @@ class StudyModel(db.Model):
self.title = pbs.TITLE
self.user_uid = pbs.NETBADGEID
self.last_updated = pbs.DATE_MODIFIED
self.protocol_builder_status = ProtocolBuilderStatus.INCOMPLETE
if pbs.Q_COMPLETE:
self.protocol_builder_status = ProtocolBuilderStatus.ACTIVE
self.protocol_builder_status = ProtocolBuilderStatus.ACTIVE
if pbs.HSRNUMBER:
self.protocol_builder_status = ProtocolBuilderStatus.OPEN
if self.on_hold:

64
crc/models/task_event.py Normal file
View File

@ -0,0 +1,64 @@
from marshmallow import INCLUDE, fields
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from crc import db, ma
from crc.models.study import StudyModel, StudySchema, WorkflowMetadataSchema, WorkflowMetadata
from crc.models.workflow import WorkflowModel
class TaskEventModel(db.Model):
__tablename__ = 'task_event'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=False)
user_uid = db.Column(db.String, nullable=False) # In some cases the unique user id may not exist in the db yet.
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False)
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
spec_version = db.Column(db.String)
action = db.Column(db.String)
task_id = db.Column(db.String)
task_name = db.Column(db.String)
task_title = db.Column(db.String)
task_type = db.Column(db.String)
task_state = db.Column(db.String)
task_lane = db.Column(db.String)
form_data = db.Column(db.JSON) # And form data submitted when the task was completed.
mi_type = db.Column(db.String)
mi_count = db.Column(db.Integer)
mi_index = db.Column(db.Integer)
process_name = db.Column(db.String)
date = db.Column(db.DateTime)
class TaskEventModelSchema(SQLAlchemyAutoSchema):
class Meta:
model = TaskEventModel
load_instance = True
include_relationships = True
include_fk = True # Includes foreign keys
class TaskEvent(object):
def __init__(self, model: TaskEventModel, study: StudyModel, workflow: WorkflowMetadata):
self.id = model.id
self.study = study
self.workflow = workflow
self.user_uid = model.user_uid
self.action = model.action
self.task_id = model.task_id
self.task_title = model.task_title
self.task_name = model.task_name
self.task_type = model.task_type
self.task_state = model.task_state
self.task_lane = model.task_lane
class TaskEventSchema(ma.Schema):
study = fields.Nested(StudySchema, dump_only=True)
workflow = fields.Nested(WorkflowMetadataSchema, dump_only=True)
class Meta:
model = TaskEvent
additional = ["id", "user_uid", "action", "task_id", "task_title",
"task_name", "task_type", "task_state", "task_lane"]
unknown = INCLUDE

View File

@ -35,7 +35,7 @@ class UserModel(db.Model):
}
return jwt.encode(
payload,
app.config.get('TOKEN_AUTH_SECRET_KEY'),
app.config.get('SECRET_KEY'),
algorithm='HS256',
)
@ -47,7 +47,7 @@ class UserModel(db.Model):
:return: integer|string
"""
try:
payload = jwt.decode(auth_token, app.config.get('TOKEN_AUTH_SECRET_KEY'), algorithms='HS256')
payload = jwt.decode(auth_token, app.config.get('SECRET_KEY'), algorithms='HS256')
return payload
except jwt.ExpiredSignatureError:
raise ApiError('token_expired', 'The Authentication token you provided expired and must be renewed.')

90
crc/scripts/email.py Normal file
View File

@ -0,0 +1,90 @@
import markdown
from jinja2 import Template
from crc import app
from crc.api.common import ApiError
from crc.scripts.script import Script
from crc.services.ldap_service import LdapService
from crc.services.mails import send_mail
class Email(Script):
"""This Script allows to be introduced as part of a workflow and called from there, specifying
recipients and content """
def get_description(self):
return """
Creates an email, using the provided arguments (a list of UIDs)"
Each argument will be used to look up personal information needed for
the email creation.
Example:
Email Subject ApprvlApprvr1 PIComputingID
"""
def do_task_validate_only(self, task, *args, **kwargs):
self.get_subject(task, args)
self.get_users_info(task, args)
self.get_content(task)
def do_task(self, task, *args, **kwargs):
args = [arg for arg in args if type(arg) == str]
subject = self.get_subject(task, args)
recipients = self.get_users_info(task, args)
content, content_html = self.get_content(task)
if recipients:
send_mail(
subject=subject,
sender=app.config['DEFAULT_SENDER'],
recipients=recipients,
content=content,
content_html=content_html
)
def get_users_info(self, task, args):
if len(args) < 1:
raise ApiError(code="missing_argument",
message="Email script requires at least one argument. The "
"name of the variable in the task data that contains user"
"id to process. Multiple arguments are accepted.")
emails = []
for arg in args:
try:
uid = task.workflow.script_engine.evaluate_expression(task, arg)
except Exception as e:
app.logger.error(f'Workflow engines could not parse {arg}', exc_info=True)
continue
user_info = LdapService.user_info(uid)
email = user_info.email_address
emails.append(user_info.email_address)
if not isinstance(email, str):
raise ApiError(code="invalid_argument",
message="The Email script requires at least 1 UID argument. The "
"name of the variable in the task data that contains subject and"
" user ids to process. This must point to an array or a string, but "
"it currently points to a %s " % emails.__class__.__name__)
return emails
def get_subject(self, task, args):
if len(args) < 1:
raise ApiError(code="missing_argument",
message="Email script requires at least one subject argument. The "
"name of the variable in the task data that contains subject"
" to process. Multiple arguments are accepted.")
subject = args[0]
if not isinstance(subject, str):
raise ApiError(code="invalid_argument",
message="The Email script requires 1 argument. The "
"the name of the variable in the task data that contains user"
"ids to process. This must point to an array or a string, but "
"it currently points to a %s " % subject.__class__.__name__)
return subject
def get_content(self, task):
content = task.task_spec.documentation
template = Template(content)
rendered = template.render(task.data)
rendered_markdown = markdown.markdown(rendered).replace('\n', '<br>')
return rendered, rendered_markdown

View File

@ -5,7 +5,7 @@ from crc.scripts.script import Script
class FactService(Script):
def get_description(self):
return """Just your basic class that can pull in data from a few api endpoints and
return """Just your basic class that can pull in data from a few api endpoints and
do a basic task."""
def get_cat(self):

View File

@ -14,7 +14,7 @@ class StudyInfo(Script):
"""Please see the detailed description that is provided below. """
pb = ProtocolBuilderService()
type_options = ['info', 'investigators', 'details', 'approvals', 'documents', 'protocol']
type_options = ['info', 'investigators', 'roles', 'details', 'approvals', 'documents', 'protocol']
# This is used for test/workflow validation, as well as documentation.
example_data = {
@ -106,11 +106,20 @@ Returns the basic information such as the id and title
### Investigators ###
Returns detailed information about related personnel.
The order returned is guaranteed to match the order provided in the investigators.xslx reference file.
If possible, detailed information is added in from LDAP about each personnel based on their user_id.
Detailed information is added in from LDAP about each personnel based on their user_id.
```
{investigators_example}
```
### Investigator Roles ###
Returns a list of all investigator roles, populating any roles with additional information available from
the Protocol Builder and LDAP. Its basically just like Investigators, but it includes all the roles, rather
that just those that were set in Protocol Builder.
```
{investigators_example}
```
### Details ###
Returns detailed information about variable keys read in from the Protocol Builder.
@ -161,6 +170,12 @@ Returns information specific to the protocol.
"INVESTIGATORTYPEFULL": "Primary Investigator",
"NETBADGEID": "dhf8r"
},
"roles":
{
"INVESTIGATORTYPE": "PI",
"INVESTIGATORTYPEFULL": "Primary Investigator",
"NETBADGEID": "dhf8r"
},
"details":
{
"IS_IND": 0,
@ -177,7 +192,7 @@ Returns information specific to the protocol.
"workflow_spec_id": "irb_api_details",
},
'protocol': {
id: 0,
'id': 0,
}
}
}
@ -198,6 +213,8 @@ Returns information specific to the protocol.
self.add_data_to_task(task, {cmd: schema.dump(study)})
if cmd == 'investigators':
self.add_data_to_task(task, {cmd: StudyService().get_investigators(study_id)})
if cmd == 'roles':
self.add_data_to_task(task, {cmd: StudyService().get_investigators(study_id, all=True)})
if cmd == 'details':
self.add_data_to_task(task, {cmd: self.pb.get_study_details(study_id)})
if cmd == 'approvals':

View File

@ -1,6 +1,10 @@
from datetime import datetime
import json
import pickle
import sys
from base64 import b64decode
from datetime import datetime, timedelta
from sqlalchemy import desc
from sqlalchemy import desc, func
from crc import app, db, session
from crc.api.common import ApiError
@ -109,16 +113,135 @@ class ApprovalService(object):
db_approvals = query.all()
return [Approval.from_model(approval_model) for approval_model in db_approvals]
@staticmethod
def get_approval_details(approval):
"""Returns a list of packed approval details, obtained from
the task data sent during the workflow """
def extract_value(task, key):
if key in task['data']:
return pickle.loads(b64decode(task['data'][key]['__bytes__']))
else:
return ""
def find_task(uuid, task):
if task['id']['__uuid__'] == uuid:
return task
for child in task['children']:
task = find_task(uuid, child)
if task:
return task
if approval.status != ApprovalStatus.APPROVED.value:
return {}
for related_approval in approval.related_approvals:
if related_approval.status != ApprovalStatus.APPROVED.value:
continue
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == approval.workflow_id).first()
data = json.loads(workflow.bpmn_workflow_json)
last_task = find_task(data['last_task']['__uuid__'], data['task_tree'])
personnel = extract_value(last_task, 'personnel')
training_val = extract_value(last_task, 'RequiredTraining')
pi_supervisor = extract_value(last_task, 'PISupervisor')['value']
review_complete = 'AllRequiredTraining' in training_val
pi_uid = workflow.study.primary_investigator_id
pi_details = LdapService.user_info(pi_uid)
details = {
'Supervisor': pi_supervisor,
'PI_Details': pi_details,
'Review': review_complete
}
details['person_details'] = []
details['person_details'].append(pi_details)
for person in personnel:
uid = person['PersonnelComputingID']['value']
details['person_details'].append(LdapService.user_info(uid))
return details
@staticmethod
def get_health_attesting_records():
"""Return a list with prepared information related to all approvals """
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
health_attesting_rows = [
['university_computing_id',
'last_name',
'first_name',
'department',
'job_title',
'supervisor_university_computing_id']
]
for approval in approvals:
try:
details = ApprovalService.get_approval_details(approval)
if not details:
continue
for person in details['person_details']:
first_name = person.given_name
last_name = person.display_name.replace(first_name, '').strip()
record = [
person.uid,
last_name,
first_name,
'',
'Academic Researcher',
details['Supervisor'] if person.uid == details['person_details'][0].uid else 'askresearch'
]
if record not in health_attesting_rows:
health_attesting_rows.append(record)
except Exception as e:
app.logger.error(f'Error pulling data for workflow {approval.workflow_id}', exc_info=True)
return health_attesting_rows
@staticmethod
def get_not_really_csv_content():
approvals = ApprovalService.get_all_approvals(include_cancelled=False)
output = []
errors = []
for approval in approvals:
try:
details = ApprovalService.get_approval_details(approval)
for person in details['person_details']:
record = {
"study_id": approval.study_id,
"pi_uid": details['PI_Details'].uid,
"pi": details['PI_Details'].display_name,
"name": person.display_name,
"uid": person.uid,
"email": person.email_address,
"supervisor": details['Supervisor'] if person.uid == details['person_details'][0].uid else "",
"review_complete": details['Review'],
}
output.append(record)
except Exception as e:
errors.append(
f'Error pulling data for workflow #{approval.workflow_id} '
f'(Approval status: {approval.status} - '
f'More details in Sentry): {str(e)}'
)
# Detailed information sent to Sentry
app.logger.error(f'Error pulling data for workflow {approval.workflow_id}', exc_info=True)
return {"results": output, "errors": errors }
@staticmethod
def update_approval(approval_id, approver_uid):
"""Update a specific approval"""
"""Update a specific approval
NOTE: Actual update happens in the API layer, this
funtion is currently in charge of only sending
corresponding emails
"""
db_approval = session.query(ApprovalModel).get(approval_id)
status = db_approval.status
if db_approval:
# db_approval.status = status
# session.add(db_approval)
# session.commit()
if status == ApprovalStatus.APPROVED.value:
# second_approval = ApprovalModel().query.filter_by(
# study_id=db_approval.study_id, workflow_id=db_approval.workflow_id,
@ -135,7 +258,7 @@ class ApprovalService(object):
f'{approver_info.display_name} - ({approver_info.uid})'
)
if mail_result:
app.logger.error(mail_result)
app.logger.error(mail_result, exc_info=True)
elif status == ApprovalStatus.DECLINED.value:
ldap_service = LdapService()
pi_user_info = ldap_service.user_info(db_approval.study.primary_investigator_id)
@ -147,7 +270,7 @@ class ApprovalService(object):
f'{approver_info.display_name} - ({approver_info.uid})'
)
if mail_result:
app.logger.error(mail_result)
app.logger.error(mail_result, exc_info=True)
first_approval = ApprovalModel().query.filter_by(
study_id=db_approval.study_id, workflow_id=db_approval.workflow_id,
status=ApprovalStatus.APPROVED.value, version=db_approval.version).first()
@ -163,8 +286,8 @@ class ApprovalService(object):
f'{approver_info.display_name} - ({approver_info.uid})'
)
if mail_result:
app.logger.error(mail_result)
# TODO: Log update action by approver_uid - maybe ?
app.logger.error(mail_result, exc_info=True)
return db_approval
@staticmethod
@ -176,11 +299,12 @@ class ApprovalService(object):
pending approvals and create a new approval for the latest version
of the workflow."""
# Find any existing approvals for this workflow and approver.
latest_approval_request = db.session.query(ApprovalModel). \
# Find any existing approvals for this workflow.
latest_approval_requests = db.session.query(ApprovalModel). \
filter(ApprovalModel.workflow_id == workflow_id). \
filter(ApprovalModel.approver_uid == approver_uid). \
order_by(desc(ApprovalModel.version)).first()
order_by(desc(ApprovalModel.version))
latest_approver_request = latest_approval_requests.filter(ApprovalModel.approver_uid == approver_uid).first()
# Construct as hash of the latest files to see if things have changed since
# the last approval.
@ -195,16 +319,20 @@ class ApprovalService(object):
# If an existing approval request exists and no changes were made, do nothing.
# If there is an existing approval request for a previous version of the workflow
# then add a new request, and cancel any waiting/pending requests.
if latest_approval_request:
request_file_ids = list(file.file_data_id for file in latest_approval_request.approval_files)
if latest_approver_request:
request_file_ids = list(file.file_data_id for file in latest_approver_request.approval_files)
current_data_file_ids.sort()
request_file_ids.sort()
other_approver = latest_approval_requests.filter(ApprovalModel.approver_uid != approver_uid).first()
if current_data_file_ids == request_file_ids:
return # This approval already exists.
return # This approval already exists or we're updating other approver.
else:
latest_approval_request.status = ApprovalStatus.CANCELED.value
db.session.add(latest_approval_request)
version = latest_approval_request.version + 1
for approval_request in latest_approval_requests:
if (approval_request.version == latest_approver_request.version and
approval_request.status != ApprovalStatus.CANCELED.value):
approval_request.status = ApprovalStatus.CANCELED.value
db.session.add(approval_request)
version = latest_approver_request.version + 1
else:
version = 1
@ -234,7 +362,7 @@ class ApprovalService(object):
f'{approver_info.display_name} - ({approver_info.uid})'
)
if mail_result:
app.logger.error(mail_result)
app.logger.error(mail_result, exc_info=True)
# send rrp approval request for first approver
# enhance the second part in case it bombs
approver_email = [approver_info.email_address] if approver_info.email_address else app.config['FALLBACK_EMAILS']
@ -244,7 +372,7 @@ class ApprovalService(object):
f'{pi_user_info.display_name} - ({pi_user_info.uid})'
)
if mail_result:
app.logger.error(mail_result)
app.logger.error(mail_result, exc_info=True)
@staticmethod
def _create_approval_files(workflow_data_files, approval):

View File

@ -0,0 +1,43 @@
from datetime import datetime
from flask_mail import Message
from sqlalchemy import desc
from crc import app, db, mail, session
from crc.api.common import ApiError
from crc.models.study import StudyModel
from crc.models.email import EmailModel
class EmailService(object):
"""Provides common tools for working with an Email"""
@staticmethod
def add_email(subject, sender, recipients, content, content_html, study_id=None):
"""We will receive all data related to an email and store it"""
# Find corresponding study - if any
study = None
if type(study_id) == int:
study = db.session.query(StudyModel).get(study_id)
# Create EmailModel
email_model = EmailModel(subject=subject, sender=sender, recipients=str(recipients),
content=content, content_html=content_html, study=study)
# Send mail
try:
msg = Message(subject,
sender=sender,
recipients=recipients)
msg.body = content
msg.html = content_html
mail.send(msg)
except Exception as e:
app.logger.error('An exception happened in EmailService', exc_info=True)
app.logger.error(str(e))
db.session.add(email_model)
db.session.commit()

View File

@ -3,7 +3,7 @@ import json
import os
from datetime import datetime
from uuid import UUID
from xml.etree import ElementTree
from lxml import etree
import flask
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
@ -58,7 +58,7 @@ class FileService(object):
"irb_docunents.xslx reference file. This code is not found in that file '%s'" % irb_doc_code)
"""Assure this is unique to the workflow, task, and document code AND the Name
Because we will allow users to upload multiple files for the same form field
Because we will allow users to upload multiple files for the same form field
in some cases """
file_model = session.query(FileModel)\
.filter(FileModel.workflow_id == workflow_id)\
@ -151,7 +151,7 @@ class FileService(object):
# If this is a BPMN, extract the process id.
if file_model.type == FileType.bpmn:
bpmn: ElementTree.Element = ElementTree.fromstring(binary_data)
bpmn: etree.Element = etree.fromstring(binary_data)
file_model.primary_process_id = FileService.get_process_id(bpmn)
new_file_data_model = FileDataModel(
@ -165,7 +165,7 @@ class FileService(object):
return file_model
@staticmethod
def get_process_id(et_root: ElementTree.Element):
def get_process_id(et_root: etree.Element):
process_elements = []
for child in et_root:
if child.tag.endswith('process') and child.attrib.get('isExecutable', False):
@ -179,7 +179,7 @@ class FileService(object):
# Look for the element that has the startEvent in it
for e in process_elements:
this_element: ElementTree.Element = e
this_element: etree.Element = e
for child_element in list(this_element):
if child_element.tag.endswith('startEvent'):
return this_element.attrib['id']

View File

@ -1,8 +1,10 @@
import logging
import re
from collections import OrderedDict
from pandas import ExcelFile
from sqlalchemy import func, desc
import pandas as pd
from pandas import ExcelFile, np
from sqlalchemy import desc
from sqlalchemy.sql.functions import GenericFunction
from crc import db
@ -19,8 +21,8 @@ class TSRank(GenericFunction):
package = 'full_text'
name = 'ts_rank'
class LookupService(object):
class LookupService(object):
"""Provides tools for doing lookups for auto-complete fields.
This can currently take two forms:
1) Lookup from spreadsheet data associated with a workflow specification.
@ -44,61 +46,68 @@ class LookupService(object):
def __get_lookup_model(workflow, field_id):
lookup_model = db.session.query(LookupFileModel) \
.filter(LookupFileModel.workflow_spec_id == workflow.workflow_spec_id) \
.filter(LookupFileModel.field_id == field_id).first()
.filter(LookupFileModel.field_id == field_id) \
.order_by(desc(LookupFileModel.id)).first()
# one more quick query, to see if the lookup file is still related to this workflow.
# if not, we need to rebuild the lookup table.
is_current = False
if lookup_model:
is_current = db.session.query(WorkflowSpecDependencyFile).\
filter(WorkflowSpecDependencyFile.file_data_id == lookup_model.file_data_model_id).count()
is_current = db.session.query(WorkflowSpecDependencyFile). \
filter(WorkflowSpecDependencyFile.file_data_id == lookup_model.file_data_model_id).\
filter(WorkflowSpecDependencyFile.workflow_id == workflow.id).count()
if not is_current:
if lookup_model:
db.session.delete(lookup_model)
# Very very very expensive, but we don't know need this till we do.
lookup_model = LookupService.create_lookup_model(workflow, field_id)
return lookup_model
@staticmethod
def lookup(workflow, field_id, query, limit):
def lookup(workflow, field_id, query, value=None, limit=10):
lookup_model = LookupService.__get_lookup_model(workflow, field_id)
if lookup_model.is_ldap:
return LookupService._run_ldap_query(query, limit)
else:
return LookupService._run_lookup_query(lookup_model, query, limit)
return LookupService._run_lookup_query(lookup_model, query, value, limit)
@staticmethod
def create_lookup_model(workflow_model, field_id):
"""
This is all really expensive, but should happen just once (per file change).
Checks to see if the options are provided in a separate lookup table associated with the
workflow, and if so, assures that data exists in the database, and return a model than can be used
to locate that data.
Returns: an array of LookupData, suitable for returning to the api.
This is all really expensive, but should happen just once (per file change).
Checks to see if the options are provided in a separate lookup table associated with the workflow, and if so,
assures that data exists in the database, and return a model than can be used to locate that data.
Returns: an array of LookupData, suitable for returning to the API.
"""
processor = WorkflowProcessor(workflow_model) # VERY expensive, Ludicrous for lookup / type ahead
spiff_task, field = processor.find_task_and_field_by_field_id(field_id)
if field.has_property(Task.PROP_OPTIONS_FILE):
if not field.has_property(Task.PROP_OPTIONS_VALUE_COLUMN) or \
not field.has_property(Task.PROP_OPTIONS_LABEL_COL):
raise ApiError.from_task("invalid_emum",
# Clear out all existing lookup models for this workflow and field.
existing_models = db.session.query(LookupFileModel) \
.filter(LookupFileModel.workflow_spec_id == workflow_model.workflow_spec_id) \
.filter(LookupFileModel.field_id == field_id).all()
for model in existing_models: # Do it one at a time to cause the required cascade of deletes.
db.session.delete(model)
# Use the contents of a file to populate enum field options
if field.has_property(Task.PROP_OPTIONS_FILE_NAME):
if not (field.has_property(Task.PROP_OPTIONS_FILE_VALUE_COLUMN) or
field.has_property(Task.PROP_OPTIONS_FILE_LABEL_COLUMN)):
raise ApiError.from_task("invalid_enum",
"For enumerations based on an xls file, you must include 3 properties: %s, "
"%s, and %s" % (Task.PROP_OPTIONS_FILE,
Task.PROP_OPTIONS_VALUE_COLUMN,
Task.PROP_OPTIONS_LABEL_COL),
"%s, and %s" % (Task.PROP_OPTIONS_FILE_NAME,
Task.PROP_OPTIONS_FILE_VALUE_COLUMN,
Task.PROP_OPTIONS_FILE_LABEL_COLUMN),
task=spiff_task)
# Get the file data from the File Service
file_name = field.get_property(Task.PROP_OPTIONS_FILE)
value_column = field.get_property(Task.PROP_OPTIONS_VALUE_COLUMN)
label_column = field.get_property(Task.PROP_OPTIONS_LABEL_COL)
file_name = field.get_property(Task.PROP_OPTIONS_FILE_NAME)
value_column = field.get_property(Task.PROP_OPTIONS_FILE_VALUE_COLUMN)
label_column = field.get_property(Task.PROP_OPTIONS_FILE_LABEL_COLUMN)
latest_files = FileService.get_spec_data_files(workflow_spec_id=workflow_model.workflow_spec_id,
workflow_id=workflow_model.id,
name=file_name)
@ -110,14 +119,15 @@ class LookupService(object):
lookup_model = LookupService.build_lookup_table(data_model, value_column, label_column,
workflow_model.workflow_spec_id, field_id)
# Use the results of an LDAP request to populate enum field options
elif field.has_property(Task.PROP_LDAP_LOOKUP):
lookup_model = LookupFileModel(workflow_spec_id=workflow_model.workflow_spec_id,
field_id=field_id,
is_ldap=True)
else:
raise ApiError("unknown_lookup_option",
"Lookup supports using spreadsheet options or ldap options, and neither "
"was provided.")
"Lookup supports using spreadsheet or LDAP options, "
"and neither of those was provided.")
db.session.add(lookup_model)
db.session.commit()
return lookup_model
@ -130,12 +140,13 @@ class LookupService(object):
changed. """
xls = ExcelFile(data_model.data)
df = xls.parse(xls.sheet_names[0]) # Currently we only look at the fist sheet.
df = pd.DataFrame(df).replace({np.nan: None})
if value_column not in df:
raise ApiError("invalid_emum",
raise ApiError("invalid_enum",
"The file %s does not contain a column named % s" % (data_model.file_model.name,
value_column))
if label_column not in df:
raise ApiError("invalid_emum",
raise ApiError("invalid_enum",
"The file %s does not contain a column named % s" % (data_model.file_model.name,
label_column))
@ -149,39 +160,40 @@ class LookupService(object):
lookup_data = LookupDataModel(lookup_file_model=lookup_model,
value=row[value_column],
label=row[label_column],
data=row.to_json())
data=row.to_dict(OrderedDict))
db.session.add(lookup_data)
db.session.commit()
return lookup_model
@staticmethod
def _run_lookup_query(lookup_file_model, query, limit):
def _run_lookup_query(lookup_file_model, query, value, limit):
db_query = LookupDataModel.query.filter(LookupDataModel.lookup_file_model == lookup_file_model)
if value is not None: # Then just find the model with that value
db_query = db_query.filter(LookupDataModel.value == value)
else:
# Build a full text query that takes all the terms provided and executes each term as a prefix query, and
# OR's those queries together. The order of the results is handled as a standard "Like" on the original
# string which seems to work intuitively for most entries.
query = re.sub('[^A-Za-z0-9 ]+', '', query) # Strip out non ascii characters.
query = re.sub(r'\s+', ' ', query) # Convert multiple space like characters to just one space, as we split on spaces.
print("Query: " + query)
query = query.strip()
if len(query) > 0:
if ' ' in query:
terms = query.split(' ')
new_terms = ["'%s'" % query]
for t in terms:
new_terms.append("%s:*" % t)
new_query = ' | '.join(new_terms)
else:
new_query = "%s:*" % query
query = re.sub('[^A-Za-z0-9 ]+', '', query)
print("Query: " + query)
query = query.strip()
if len(query) > 0:
if ' ' in query:
terms = query.split(' ')
new_terms = ["'%s'" % query]
for t in terms:
new_terms.append("%s:*" % t)
new_query = ' | '.join(new_terms)
else:
new_query = "%s:*" % query
# Run the full text query
db_query = db_query.filter(LookupDataModel.label.match(new_query))
# But hackishly order by like, which does a good job of
# pulling more relevant matches to the top.
db_query = db_query.order_by(desc(LookupDataModel.label.like("%" + query + "%")))
# Run the full text query
db_query = db_query.filter(LookupDataModel.label.match(new_query))
# But hackishly order by like, which does a good job of
# pulling more relevant matches to the top.
db_query = db_query.order_by(desc(LookupDataModel.label.like("%" + query + "%")))
#ORDER BY name LIKE concat('%', ticker, '%') desc, rank DESC
# db_query = db_query.order_by(desc(func.full_text.ts_rank(
# func.to_tsvector(LookupDataModel.label),
# func.to_tsquery(query))))
from sqlalchemy.dialects import postgresql
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
result = db_query.limit(limit).all()
logging.getLogger('sqlalchemy.engine').setLevel(logging.ERROR)
@ -196,8 +208,9 @@ class LookupService(object):
we return a lookup data model."""
user_list = []
for user in users:
user_list.append( {"value": user['uid'],
"label": user['display_name'] + " (" + user['uid'] + ")",
"data": user
})
return user_list
user_list.append({"value": user['uid'],
"label": user['display_name'] + " (" + user['uid'] + ")",
"data": user
})
return user_list

View File

@ -1,16 +1,20 @@
import os
from flask import render_template, render_template_string
from flask_mail import Message
from jinja2 import Environment, FileSystemLoader
from crc import app, mail
from crc.services.email_service import EmailService
# Jinja environment definition, used to render mail templates
template_dir = app.root_path + '/static/templates/mails'
env = Environment(loader=FileSystemLoader(template_dir))
# TODO: Extract common mailing code into its own function
def send_test_email(sender, recipients):
try:
msg = Message('Research Ramp-up Plan test',
sender=sender,
recipients=recipients)
from crc import env, mail
sender=sender,
recipients=recipients,
bcc=['rrt_emails@googlegroups.com'])
template = env.get_template('ramp_up_approval_request_first_review.txt')
template_vars = {'primary_investigator': "test"}
msg.body = template.render(template_vars)
@ -21,108 +25,78 @@ def send_test_email(sender, recipients):
return str(e)
def send_mail(subject, sender, recipients, content, content_html, study_id=None):
EmailService.add_email(subject=subject, sender=sender, recipients=recipients,
content=content, content_html=content_html, study_id=study_id)
def send_ramp_up_submission_email(sender, recipients, approver_1, approver_2=None):
try:
msg = Message('Research Ramp-up Plan Submitted',
sender=sender,
recipients=recipients,
bcc=['rrt_emails@googlegroups.com'])
from crc import env, mail
template = env.get_template('ramp_up_submission.txt')
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_submission.html')
msg.html = template.render(template_vars)
subject = 'Research Ramp-up Plan Submitted'
template = env.get_template('ramp_up_submission.txt')
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
content = template.render(template_vars)
template = env.get_template('ramp_up_submission.html')
content_html = template.render(template_vars)
send_mail(subject, sender, recipients, content, content_html)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_approval_request_email(sender, recipients, primary_investigator):
try:
msg = Message('Research Ramp-up Plan Approval Request',
sender=sender,
recipients=recipients,
bcc=['rrt_emails@googlegroups.com'])
from crc import env, mail
template = env.get_template('ramp_up_approval_request.txt')
template_vars = {'primary_investigator': primary_investigator}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_approval_request.html')
msg.html = template.render(template_vars)
subject = 'Research Ramp-up Plan Approval Request'
template = env.get_template('ramp_up_approval_request.txt')
template_vars = {'primary_investigator': primary_investigator}
content = template.render(template_vars)
template = env.get_template('ramp_up_approval_request.html')
content_html = template.render(template_vars)
send_mail(subject, sender, recipients, content, content_html)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_approval_request_first_review_email(sender, recipients, primary_investigator):
try:
msg = Message('Research Ramp-up Plan Approval Request',
sender=sender,
recipients=recipients,
bcc=['rrt_emails@googlegroups.com'])
from crc import env, mail
template = env.get_template('ramp_up_approval_request_first_review.txt')
template_vars = {'primary_investigator': primary_investigator}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_approval_request_first_review.html')
msg.html = template.render(template_vars)
subject = 'Research Ramp-up Plan Approval Request'
template = env.get_template('ramp_up_approval_request_first_review.txt')
template_vars = {'primary_investigator': primary_investigator}
content = template.render(template_vars)
template = env.get_template('ramp_up_approval_request_first_review.html')
content_html = template.render(template_vars)
send_mail(subject, sender, recipients, content, content_html)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_approved_email(sender, recipients, approver_1, approver_2=None):
try:
msg = Message('Research Ramp-up Plan Approved',
sender=sender,
recipients=recipients,
bcc=['rrt_emails@googlegroups.com'])
subject = 'Research Ramp-up Plan Approved'
from crc import env, mail
template = env.get_template('ramp_up_approved.txt')
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_approved.html')
msg.html = template.render(template_vars)
template = env.get_template('ramp_up_approved.txt')
template_vars = {'approver_1': approver_1, 'approver_2': approver_2}
content = template.render(template_vars)
template = env.get_template('ramp_up_approved.html')
content_html = template.render(template_vars)
send_mail(subject, sender, recipients, content, content_html)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_denied_email(sender, recipients, approver):
try:
msg = Message('Research Ramp-up Plan Denied',
sender=sender,
recipients=recipients,
bcc=['rrt_emails@googlegroups.com'])
subject = 'Research Ramp-up Plan Denied'
from crc import env, mail
template = env.get_template('ramp_up_denied.txt')
template_vars = {'approver': approver}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_denied.html')
msg.html = template.render(template_vars)
template = env.get_template('ramp_up_denied.txt')
template_vars = {'approver': approver}
content = template.render(template_vars)
template = env.get_template('ramp_up_denied.html')
content_html = template.render(template_vars)
send_mail(subject, sender, recipients, content, content_html)
mail.send(msg)
except Exception as e:
return str(e)
def send_ramp_up_denied_email_to_approver(sender, recipients, primary_investigator, approver_2):
try:
msg = Message('Research Ramp-up Plan Denied',
sender=sender,
recipients=recipients,
bcc=['rrt_emails@googlegroups.com'])
subject = 'Research Ramp-up Plan Denied'
from crc import env, mail
template = env.get_template('ramp_up_denied_first_approver.txt')
template_vars = {'primary_investigator': primary_investigator, 'approver_2': approver_2}
msg.body = template.render(template_vars)
template = env.get_template('ramp_up_denied_first_approver.html')
msg.html = template.render(template_vars)
template = env.get_template('ramp_up_denied_first_approver.txt')
template_vars = {'primary_investigator': primary_investigator, 'approver_2': approver_2}
content = template.render(template_vars)
template = env.get_template('ramp_up_denied_first_approver.html')
content_html = template.render(template_vars)
mail.send(msg)
except Exception as e:
return str(e)
send_mail(subject, sender, recipients, content, content_html)

View File

@ -1,3 +1,4 @@
from copy import copy
from datetime import datetime
import json
from typing import List
@ -12,7 +13,7 @@ from crc.api.common import ApiError
from crc.models.file import FileModel, FileModelSchema, File
from crc.models.ldap import LdapSchema
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel, Study, Category, WorkflowMetadata
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus
@ -64,13 +65,15 @@ class StudyService(object):
study.files = list(files)
# Calling this line repeatedly is very very slow. It creates the
# master spec and runs it.
status = StudyService.__get_study_status(study_model)
study.warnings = StudyService.__update_status_of_workflow_meta(workflow_metas, status)
# master spec and runs it. Don't execute this for Abandoned studies, as
# we don't have the information to process them.
if study.protocol_builder_status != ProtocolBuilderStatus.ABANDONED:
status = StudyService.__get_study_status(study_model)
study.warnings = StudyService.__update_status_of_workflow_meta(workflow_metas, status)
# Group the workflows into their categories.
for category in study.categories:
category.workflows = {w for w in workflow_metas if w.category_id == category.id}
# Group the workflows into their categories.
for category in study.categories:
category.workflows = {w for w in workflow_metas if w.category_id == category.id}
return study
@ -137,7 +140,7 @@ class StudyService(object):
try:
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
except requests.exceptions.ConnectionError as ce:
app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce))
app.logger.error(f'Failed to connect to the Protocol Builder - {str(ce)}', exc_info=True)
pb_docs = []
else:
pb_docs = []
@ -181,10 +184,9 @@ class StudyService(object):
documents[code] = doc
return documents
@staticmethod
def get_investigators(study_id):
def get_investigators(study_id, all=False):
"""Convert array of investigators from protocol builder into a dictionary keyed on the type. """
# Loop through all known investigator types as set in the reference file
inv_dictionary = FileService.get_reference_data(FileService.INVESTIGATOR_LIST, 'code')
@ -192,16 +194,26 @@ class StudyService(object):
# Get PB required docs
pb_investigators = ProtocolBuilderService.get_investigators(study_id=study_id)
"""Convert array of investigators from protocol builder into a dictionary keyed on the type"""
# It is possible for the same type to show up more than once in some circumstances, in those events
# append a counter to the name.
investigators = {}
for i_type in inv_dictionary:
pb_data = next((item for item in pb_investigators if item['INVESTIGATORTYPE'] == i_type), None)
if pb_data:
inv_dictionary[i_type]['user_id'] = pb_data["NETBADGEID"]
inv_dictionary[i_type].update(StudyService.get_ldap_dict_if_available(pb_data["NETBADGEID"]))
else:
inv_dictionary[i_type]['user_id'] = None
return inv_dictionary
pb_data_entries = list(item for item in pb_investigators if item['INVESTIGATORTYPE'] == i_type)
entry_count = 0
investigators[i_type] = copy(inv_dictionary[i_type])
investigators[i_type]['user_id'] = None
for pb_data in pb_data_entries:
entry_count += 1
if entry_count == 1:
t = i_type
else:
t = i_type + "_" + str(entry_count)
investigators[t] = copy(inv_dictionary[i_type])
investigators[t]['user_id'] = pb_data["NETBADGEID"]
investigators[t].update(StudyService.get_ldap_dict_if_available(pb_data["NETBADGEID"]))
if not all:
investigators = dict(filter(lambda elem: elem[1]['user_id'] is not None, investigators.items()))
return investigators
@staticmethod
def get_ldap_dict_if_available(user_id):
@ -224,7 +236,6 @@ class StudyService(object):
return FileModelSchema().dump(file)
@staticmethod
def synch_with_protocol_builder_if_enabled(user):
"""Assures that the studies we have locally for the given user are

View File

@ -1,5 +1,8 @@
import re
import xml.etree.ElementTree as ElementTree
from SpiffWorkflow.serializer.exceptions import MissingSpecError
from lxml import etree
import shlex
from datetime import datetime
from typing import List
@ -13,14 +16,14 @@ from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
from SpiffWorkflow.exceptions import WorkflowTaskExecException
from SpiffWorkflow.specs import WorkflowSpec
from sqlalchemy import desc
from crc import session
from crc import session, app
from crc.api.common import ApiError
from crc.models.file import FileDataModel, FileModel, FileType
from crc.models.workflow import WorkflowStatus, WorkflowModel, WorkflowSpecDependencyFile
from crc.scripts.script import Script
from crc.services.file_service import FileService
from crc import app
class CustomBpmnScriptEngine(BpmnScriptEngine):
@ -28,15 +31,29 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
Rather than execute arbitrary code, this assumes the script references a fully qualified python class
such as myapp.RandomFact. """
def execute(self, task: SpiffTask, script, **kwargs):
def execute(self, task: SpiffTask, script, data):
"""
Assume that the script read in from the BPMN file is a fully qualified python class. Instantiate
that class, pass in any data available to the current task so that it might act on it.
Assume that the class implements the "do_task" method.
Functions in two modes.
1. If the command is proceeded by #! then this is assumed to be a python script, and will
attempt to load that python module and execute the do_task method on that script. Scripts
must be located in the scripts package and they must extend the script.py class.
2. If not proceeded by the #! this will attempt to execute the script directly and assumes it is
valid Python.
"""
# Shlex splits the whole string while respecting double quoted strings within
if not script.startswith('#!'):
try:
super().execute(task, script, data)
except SyntaxError as e:
raise ApiError.from_task('syntax_error',
f'If you are running a pre-defined script, please'
f' proceed the script with "#!", otherwise this is assumed to be'
f' pure python: {script}, {e.msg}', task=task)
else:
self.run_predefined_script(task, script[2:], data) # strip off the first two characters.
This allows us to reference custom code from the BPMN diagram.
"""
commands = script.split(" ")
def run_predefined_script(self, task: SpiffTask, script, data):
commands = shlex.split(script)
path_and_command = commands[0].rsplit(".", 1)
if len(path_and_command) == 1:
module_name = "crc.scripts." + self.camel_to_snake(path_and_command[0])
@ -55,20 +72,20 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
if not isinstance(klass(), Script):
raise ApiError.from_task("invalid_script",
"This is an internal error. The script '%s:%s' you called " %
(module_name, class_name) +
"does not properly implement the CRC Script class.",
task=task)
"This is an internal error. The script '%s:%s' you called " %
(module_name, class_name) +
"does not properly implement the CRC Script class.",
task=task)
if task.workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY]:
"""If this is running a validation, and not a normal process, then we want to
"""If this is running a validation, and not a normal process, then we want to
mimic running the script, but not make any external calls or database changes."""
klass().do_task_validate_only(task, study_id, workflow_id, *commands[1:])
else:
klass().do_task(task, study_id, workflow_id, *commands[1:])
except ModuleNotFoundError:
raise ApiError.from_task("invalid_script",
"Unable to locate Script: '%s:%s'" % (module_name, class_name),
task=task)
"Unable to locate Script: '%s:%s'" % (module_name, class_name),
task=task)
def evaluate_expression(self, task, expression):
"""
@ -102,14 +119,15 @@ class WorkflowProcessor(object):
def __init__(self, workflow_model: WorkflowModel, soft_reset=False, hard_reset=False, validate_only=False):
"""Create a Workflow Processor based on the serialized information available in the workflow model.
If soft_reset is set to true, it will try to use the latest version of the workflow specification.
If hard_reset is set to true, it will create a new Workflow, but embed the data from the last
completed task in the previous workflow.
If soft_reset is set to true, it will try to use the latest version of the workflow specification
without resetting to the beginning of the workflow. This will work for some minor changes to the spec.
If hard_reset is set to true, it will use the latest spec, and start the workflow over from the beginning.
which should work in casees where a soft reset fails.
If neither flag is set, it will use the same version of the specification that was used to originally
create the workflow model. """
self.workflow_model = workflow_model
if soft_reset or len(workflow_model.dependencies) == 0:
if soft_reset or len(workflow_model.dependencies) == 0: # Depenencies of 0 means the workflow was never started.
self.spec_data_files = FileService.get_spec_data_files(
workflow_spec_id=workflow_model.workflow_spec_id)
else:
@ -135,7 +153,7 @@ class WorkflowProcessor(object):
workflow_model.bpmn_workflow_json = WorkflowProcessor._serializer.serialize_workflow(self.bpmn_workflow)
self.save()
except KeyError as ke:
except MissingSpecError as ke:
raise ApiError(code="unexpected_workflow_structure",
message="Failed to deserialize workflow"
" '%s' version %s, due to a mis-placed or missing task '%s'" %
@ -162,7 +180,10 @@ class WorkflowProcessor(object):
bpmn_workflow = BpmnWorkflow(spec, script_engine=self._script_engine)
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = workflow_model.study_id
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = validate_only
bpmn_workflow.do_engine_steps()
try:
bpmn_workflow.do_engine_steps()
except WorkflowException as we:
raise ApiError.from_task_spec("error_loading_workflow", str(we), we.sender)
return bpmn_workflow
def save(self):
@ -216,8 +237,6 @@ class WorkflowProcessor(object):
full_version = "v%s (%s)" % (version, files)
return full_version
def update_dependencies(self, spec_data_files):
existing_dependencies = FileService.get_spec_data_files(
workflow_spec_id=self.workflow_model.workflow_spec_id,
@ -267,12 +286,12 @@ class WorkflowProcessor(object):
for file_data in file_data_models:
if file_data.file_model.type == FileType.bpmn:
bpmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
bpmn: etree.Element = etree.fromstring(file_data.data)
if file_data.file_model.primary:
process_id = FileService.get_process_id(bpmn)
parser.add_bpmn_xml(bpmn, filename=file_data.file_model.name)
elif file_data.file_model.type == FileType.dmn:
dmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
dmn: etree.Element = etree.fromstring(file_data.data)
parser.add_dmn_xml(dmn, filename=file_data.file_model.name)
if process_id is None:
raise (ApiError(code="no_primary_bpmn_error",
@ -299,26 +318,16 @@ class WorkflowProcessor(object):
return WorkflowStatus.waiting
def hard_reset(self):
"""Recreate this workflow, but keep the data from the last completed task and add
it back into the first task. This may be useful when a workflow specification changes,
and users need to review all the prior steps, but they don't need to reenter all the previous data.
Returns the new version.
"""Recreate this workflow. This will be useful when a workflow specification changes.
"""
# Create a new workflow based on the latest specs.
self.spec_data_files = FileService.get_spec_data_files(workflow_spec_id=self.workflow_spec_id)
new_spec = WorkflowProcessor.get_spec(self.spec_data_files, self.workflow_spec_id)
new_bpmn_workflow = BpmnWorkflow(new_spec, script_engine=self._script_engine)
new_bpmn_workflow.data = self.bpmn_workflow.data
# Reset the current workflow to the beginning - which we will consider to be the first task after the root
# element. This feels a little sketchy, but I think it is safe to assume root will have one child.
first_task = self.bpmn_workflow.task_tree.children[0]
first_task.reset_token(reset_data=False)
for task in new_bpmn_workflow.get_tasks(SpiffTask.READY):
task.data = first_task.data
new_bpmn_workflow.do_engine_steps()
try:
new_bpmn_workflow.do_engine_steps()
except WorkflowException as we:
raise ApiError.from_task_spec("hard_reset_engine_steps_error", str(we), we.sender)
self.bpmn_workflow = new_bpmn_workflow
def get_status(self):

View File

@ -1,43 +1,53 @@
import copy
import json
import string
import uuid
from datetime import datetime
import random
import jinja2
from SpiffWorkflow import Task as SpiffTask, WorkflowException
from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.MultiInstanceTask import MultiInstanceTask
from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask
from SpiffWorkflow.bpmn.specs.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.specs import CancelTask, StartTask
from SpiffWorkflow.util.deep_merge import DeepMerge
from flask import g
from jinja2 import Template
from crc import db, app
from crc.api.common import ApiError
from crc.models.api_models import Task, MultiInstanceType
from crc.models.api_models import Task, MultiInstanceType, NavigationItem, NavigationItemSchema, WorkflowApi
from crc.models.file import LookupDataModel
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel, WorkflowStatus
from crc.models.workflow import WorkflowModel, WorkflowStatus, WorkflowSpecModel
from crc.services.file_service import FileService
from crc.services.lookup_service import LookupService
from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor, CustomBpmnScriptEngine
from crc.services.workflow_processor import WorkflowProcessor
class WorkflowService(object):
TASK_ACTION_COMPLETE = "Complete"
TASK_ACTION_TOKEN_RESET = "Backwards Move"
TASK_ACTION_HARD_RESET = "Restart (Hard)"
TASK_ACTION_SOFT_RESET = "Restart (Soft)"
TASK_ACTION_COMPLETE = "COMPLETE"
TASK_ACTION_TOKEN_RESET = "TOKEN_RESET"
TASK_ACTION_HARD_RESET = "HARD_RESET"
TASK_ACTION_SOFT_RESET = "SOFT_RESET"
TASK_ACTION_ASSIGNMENT = "ASSIGNMENT" # Whenever the lane changes between tasks we assign the task to specifc user.
TASK_STATE_LOCKED = "LOCKED" # When the task belongs to a different user.
"""Provides tools for processing workflows and tasks. This
should at some point, be the only way to work with Workflows, and
the workflow Processor should be hidden behind this service.
This will help maintain a structure that avoids circular dependencies.
But for now, this contains tools for converting spiff-workflow models into our
own API models with additional information and capabilities and
own API models with additional information and capabilities and
handles the testing of a workflow specification by completing it with
random selections, attempting to mimic a front end as much as possible. """
@ -89,11 +99,16 @@ class WorkflowService(object):
processor.bpmn_workflow.do_engine_steps()
tasks = processor.bpmn_workflow.get_tasks(SpiffTask.READY)
for task in tasks:
if task.task_spec.lane is not None and task.task_spec.lane not in task.data:
raise ApiError.from_task("invalid_role",
f"This task is in a lane called '{task.task_spec.lane}', The "
f" current task data must have information mapping this role to "
f" a unique user id.", task)
task_api = WorkflowService.spiff_task_to_api_task(
task,
add_docs_and_forms=True) # Assure we try to process the documenation, and raise those errors.
add_docs_and_forms=True) # Assure we try to process the documentation, and raise those errors.
WorkflowService.populate_form_with_random_data(task, task_api, required_only)
task.complete()
processor.complete_task(task)
except WorkflowException as we:
WorkflowService.delete_test_data()
raise ApiError.from_workflow_exception("workflow_validation_exception", str(we), we)
@ -128,20 +143,37 @@ class WorkflowService(object):
@staticmethod
def get_random_data_for_field(field, task):
if field.type == "enum":
has_ldap_lookup = field.has_property(Task.PROP_LDAP_LOOKUP)
has_file_lookup = field.has_property(Task.PROP_OPTIONS_FILE_NAME)
has_data_lookup = field.has_property(Task.PROP_OPTIONS_DATA_NAME)
has_lookup = has_ldap_lookup or has_file_lookup or has_data_lookup
if field.type == "enum" and not has_lookup:
# If it's a normal enum field with no lookup,
# return a random option.
if len(field.options) > 0:
random_choice = random.choice(field.options)
if isinstance(random_choice, dict):
return random.choice(field.options)['id']
choice = random.choice(field.options)
return {
'value': choice['id'],
'label': choice['name']
}
else:
# fixme: why it is sometimes an EnumFormFieldOption, and other times not?
return random_choice.id ## Assume it is an EnumFormFieldOption
# Assume it is an EnumFormFieldOption
return {
'value': random_choice.id,
'label': random_choice.name
}
else:
raise ApiError.from_task("invalid_enum", "You specified an enumeration field (%s),"
" with no options" % field.id, task)
elif field.type == "autocomplete":
elif field.type == "autocomplete" or field.type == "enum":
# If it has a lookup, get the lookup model from the spreadsheet or task data, then return a random option
# from the lookup model
lookup_model = LookupService.get_lookup_model(task, field)
if field.has_property(Task.PROP_LDAP_LOOKUP): # All ldap records get the same person.
if has_ldap_lookup: # All ldap records get the same person.
return {
"label": "dhf8r",
"value": "Dan Funk",
@ -157,9 +189,7 @@ class WorkflowService(object):
elif lookup_model:
data = db.session.query(LookupDataModel).filter(
LookupDataModel.lookup_file_model == lookup_model).limit(10).all()
options = []
for d in data:
options.append({"id": d.value, "label": d.label})
options = [{"value": d.value, "label": d.label, "data": d.data} for d in data]
return random.choice(options)
else:
raise ApiError.from_task("unknown_lookup_option", "The settings for this auto complete field "
@ -180,31 +210,111 @@ class WorkflowService(object):
def __get_options(self):
pass
@staticmethod
def _random_string(string_length=10):
"""Generate a random string of fixed length """
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(string_length))
@staticmethod
def processor_to_workflow_api(processor: WorkflowProcessor, next_task=None):
"""Returns an API model representing the state of the current workflow, if requested, and
possible, next_task is set to the current_task."""
nav_dict = processor.bpmn_workflow.get_nav_list()
# Some basic cleanup of the title for the for the navigation.
navigation = []
for nav_item in nav_dict:
spiff_task = processor.bpmn_workflow.get_task(nav_item['task_id'])
if 'description' in nav_item:
nav_item['title'] = nav_item.pop('description')
# fixme: duplicate code from the workflow_service. Should only do this in one place.
if nav_item['title'] is not None and ' ' in nav_item['title']:
nav_item['title'] = nav_item['title'].partition(' ')[2]
else:
nav_item['title'] = ""
if spiff_task:
nav_item['task'] = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
nav_item['title'] = nav_item['task'].title # Prefer the task title.
user_uids = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if 'user' not in g or not g.user or g.user.uid not in user_uids:
nav_item['state'] = WorkflowService.TASK_STATE_LOCKED
else:
nav_item['task'] = None
navigation.append(NavigationItem(**nav_item))
NavigationItemSchema().dump(nav_item)
spec = db.session.query(WorkflowSpecModel).filter_by(id=processor.workflow_spec_id).first()
workflow_api = WorkflowApi(
id=processor.get_workflow_id(),
status=processor.get_status(),
next_task=None,
navigation=navigation,
workflow_spec_id=processor.workflow_spec_id,
spec_version=processor.get_version_string(),
is_latest_spec=processor.is_latest_spec,
total_tasks=len(navigation),
completed_tasks=processor.workflow_model.completed_tasks,
last_updated=processor.workflow_model.last_updated,
title=spec.display_name
)
if not next_task: # The Next Task can be requested to be a certain task, useful for parallel tasks.
# This may or may not work, sometimes there is no next task to complete.
next_task = processor.next_task()
if next_task:
previous_form_data = WorkflowService.get_previously_submitted_data(processor.workflow_model.id, next_task)
DeepMerge.merge(next_task.data, previous_form_data)
workflow_api.next_task = WorkflowService.spiff_task_to_api_task(next_task, add_docs_and_forms=True)
# Update the state of the task to locked if the current user does not own the task.
user_uids = WorkflowService.get_users_assigned_to_task(processor, next_task)
if 'user' not in g or not g.user or g.user.uid not in user_uids:
workflow_api.next_task.state = WorkflowService.TASK_STATE_LOCKED
return workflow_api
@staticmethod
def get_previously_submitted_data(workflow_id, spiff_task):
""" If the user has completed this task previously, find the form data for the last submission."""
query = db.session.query(TaskEventModel) \
.filter_by(workflow_id=workflow_id) \
.filter_by(task_name=spiff_task.task_spec.name) \
.filter_by(action=WorkflowService.TASK_ACTION_COMPLETE)
if hasattr(spiff_task, 'internal_data') and 'runtimes' in spiff_task.internal_data:
query = query.filter_by(mi_index=spiff_task.internal_data['runtimes'])
latest_event = query.order_by(TaskEventModel.date.desc()).first()
if latest_event:
if latest_event.form_data is not None:
return latest_event.form_data
else:
missing_form_error = (
f'We have lost data for workflow {workflow_id}, '
f'task {spiff_task.task_spec.name}, it is not in the task event model, '
f'and it should be.'
)
app.logger.error("missing_form_data", missing_form_error, exc_info=True)
return {}
else:
return {}
@staticmethod
def spiff_task_to_api_task(spiff_task, add_docs_and_forms=False):
task_type = spiff_task.task_spec.__class__.__name__
if isinstance(spiff_task.task_spec, UserTask):
task_type = "UserTask"
elif isinstance(spiff_task.task_spec, ManualTask):
task_type = "ManualTask"
elif isinstance(spiff_task.task_spec, BusinessRuleTask):
task_type = "BusinessRuleTask"
elif isinstance(spiff_task.task_spec, CancelTask):
task_type = "CancelTask"
elif isinstance(spiff_task.task_spec, ScriptTask):
task_type = "ScriptTask"
elif isinstance(spiff_task.task_spec, StartTask):
task_type = "StartTask"
else:
task_type = "NoneTask"
task_types = [UserTask, ManualTask, BusinessRuleTask, CancelTask, ScriptTask, StartTask, EndEvent, StartEvent]
for t in task_types:
if isinstance(spiff_task.task_spec, t):
task_type = t.__name__
break
else:
task_type = "NoneTask"
info = spiff_task.task_info()
if info["is_looping"]:
@ -218,14 +328,20 @@ class WorkflowService(object):
props = {}
if hasattr(spiff_task.task_spec, 'extensions'):
for id, val in spiff_task.task_spec.extensions.items():
props[id] = val
for key, val in spiff_task.task_spec.extensions.items():
props[key] = val
if hasattr(spiff_task.task_spec, 'lane'):
lane = spiff_task.task_spec.lane
else:
lane = None
task = Task(spiff_task.id,
spiff_task.task_spec.name,
spiff_task.task_spec.description,
task_type,
spiff_task.get_state_name(),
lane,
None,
"",
{},
@ -243,8 +359,8 @@ class WorkflowService(object):
task.data = spiff_task.data
if hasattr(spiff_task.task_spec, "form"):
task.form = spiff_task.task_spec.form
for field in task.form.fields:
WorkflowService.process_options(spiff_task, field)
for i, field in enumerate(task.form.fields):
task.form.fields[i] = WorkflowService.process_options(spiff_task, field)
task.documentation = WorkflowService._process_documentation(spiff_task)
# All ready tasks should have a valid name, and this can be computed for
@ -257,10 +373,12 @@ class WorkflowService(object):
# 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 "display_name" in task.properties:
task.title = task.properties['display_name']
try:
task.title = spiff_task.workflow.script_engine.evaluate_expression(spiff_task, task.properties['display_name'])
except Exception as e:
app.logger.error("Failed to set title on task due to type error." + str(e), exc_info=True)
elif task.title and ' ' in task.title:
task.title = task.title.partition(' ')[2]
return task
@staticmethod
@ -271,7 +389,7 @@ class WorkflowService(object):
template = Template(v)
props[k] = template.render(**spiff_task.data)
except jinja2.exceptions.TemplateError as ue:
app.logger.error("Failed to process task property %s " % str(ue))
app.logger.error(f'Failed to process task property {str(ue)}', exc_info=True)
return props
@staticmethod
@ -301,7 +419,8 @@ class WorkflowService(object):
except TypeError as te:
raise ApiError.from_task(code="template_error", message="Error processing template for task %s: %s" %
(spiff_task.task_spec.name, str(te)), task=spiff_task)
# TODO: Catch additional errors and report back.
except Exception as e:
app.logger.error(str(e), exc_info=True)
@staticmethod
def process_options(spiff_task, field):
@ -309,23 +428,76 @@ class WorkflowService(object):
# If this is an auto-complete field, do not populate options, a lookup will happen later.
if field.type == Task.FIELD_TYPE_AUTO_COMPLETE:
pass
elif field.has_property(Task.PROP_OPTIONS_FILE):
elif field.has_property(Task.PROP_OPTIONS_FILE_NAME):
lookup_model = LookupService.get_lookup_model(spiff_task, field)
data = db.session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_model).all()
if not hasattr(field, 'options'):
field.options = []
for d in data:
field.options.append({"id": d.value, "name": d.label})
field.options.append({"id": d.value, "name": d.label, "data": d.data})
elif field.has_property(Task.PROP_OPTIONS_DATA_NAME):
field.options = WorkflowService.get_options_from_task_data(spiff_task, field)
return field
@staticmethod
def get_options_from_task_data(spiff_task, field):
if not (field.has_property(Task.PROP_OPTIONS_DATA_VALUE_COLUMN) or
field.has_property(Task.PROP_OPTIONS_DATA_LABEL_COLUMN)):
raise ApiError.from_task("invalid_enum",
f"For enumerations based on task data, you must include 3 properties: "
f"{Task.PROP_OPTIONS_DATA_NAME}, {Task.PROP_OPTIONS_DATA_VALUE_COLUMN}, "
f"{Task.PROP_OPTIONS_DATA_LABEL_COLUMN}", task=spiff_task)
prop = field.get_property(Task.PROP_OPTIONS_DATA_NAME)
if prop not in spiff_task.data:
raise ApiError.from_task("invalid_enum", f"For enumerations based on task data, task data must have "
f"a property called {prop}", task=spiff_task)
# Get the enum options from the task data
data_model = spiff_task.data[prop]
value_column = field.get_property(Task.PROP_OPTIONS_DATA_VALUE_COLUMN)
label_column = field.get_property(Task.PROP_OPTIONS_DATA_LABEL_COLUMN)
items = data_model.items() if isinstance(data_model, dict) else data_model
options = []
for item in items:
options.append({"id": item[value_column], "name": item[label_column], "data": item})
return options
@staticmethod
def update_task_assignments(processor):
"""For every upcoming user task, log a task action
that connects the assigned user(s) to that task. All
existing assignment actions for this workflow are removed from the database,
so that only the current valid actions are available. update_task_assignments
should be called whenever progress is made on a workflow."""
db.session.query(TaskEventModel). \
filter(TaskEventModel.workflow_id == processor.workflow_model.id). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).delete()
for task in processor.get_current_user_tasks():
user_ids = WorkflowService.get_users_assigned_to_task(processor, task)
for user_id in user_ids:
WorkflowService.log_task_action(user_id, processor, task, WorkflowService.TASK_ACTION_ASSIGNMENT)
@staticmethod
def get_users_assigned_to_task(processor, spiff_task):
if not hasattr(spiff_task.task_spec, 'lane') or spiff_task.task_spec.lane is None:
return [processor.workflow_model.study.user_uid]
# todo: return a list of all users that can edit the study by default
if spiff_task.task_spec.lane not in spiff_task.data:
return [] # No users are assignable to the task at this moment
lane_users = spiff_task.data[spiff_task.task_spec.lane]
if not isinstance(lane_users, list):
lane_users = [lane_users]
return lane_users
@staticmethod
def log_task_action(user_uid, processor, spiff_task, action):
task = WorkflowService.spiff_task_to_api_task(spiff_task)
workflow_model = processor.workflow_model
form_data = WorkflowService.extract_form_data(spiff_task.data, spiff_task)
task_event = TaskEventModel(
study_id=workflow_model.study_id,
study_id=processor.workflow_model.study_id,
user_uid=user_uid,
workflow_id=workflow_model.id,
workflow_spec_id=workflow_model.workflow_spec_id,
workflow_id=processor.workflow_model.id,
workflow_spec_id=processor.workflow_model.workflow_spec_id,
spec_version=processor.get_version_string(),
action=action,
task_id=task.id,
@ -333,6 +505,8 @@ class WorkflowService(object):
task_title=task.title,
task_type=str(task.type),
task_state=task.state,
task_lane=task.lane,
form_data=form_data,
mi_type=task.multi_instance_type.value, # Some tasks have a repeat behavior.
mi_count=task.multi_instance_count, # This is the number of times the task could repeat.
mi_index=task.multi_instance_index, # And the index of the currently repeating task.
@ -342,3 +516,29 @@ class WorkflowService(object):
db.session.add(task_event)
db.session.commit()
@staticmethod
def extract_form_data(latest_data, task):
"""Removes data from latest_data that would be added by the child task or any of its children."""
data = {}
if hasattr(task.task_spec, 'form'):
for field in task.task_spec.form.fields:
if field.has_property(Task.PROP_OPTIONS_READ_ONLY) and \
field.get_property(Task.PROP_OPTIONS_READ_ONLY).lower().strip() == "true":
continue # Don't add read-only data
elif field.has_property(Task.PROP_OPTIONS_REPEAT):
group = field.get_property(Task.PROP_OPTIONS_REPEAT)
if group in latest_data:
data[group] = latest_data[group]
elif isinstance(task.task_spec, MultiInstanceTask):
group = task.task_spec.elementVar
if group in latest_data:
data[group] = latest_data[group]
else:
if field.id in latest_data:
data[field.id] = latest_data[field.id]
return data

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_0be39yr" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_0be39yr" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_1cme33c" isExecutable="false">
<bpmn:parallelGateway id="ParallelGateway_0ecwf3g">
<bpmn:incoming>Flow_1wqp7vf</bpmn:incoming>
@ -212,7 +212,7 @@
<bpmn:scriptTask id="Activity_10nxpt2" name="Load Study Details">
<bpmn:incoming>SequenceFlow_1r3yrhy</bpmn:incoming>
<bpmn:outgoing>Flow_09h1imz</bpmn:outgoing>
<bpmn:script>StudyInfo details</bpmn:script>
<bpmn:script>#! StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_PBMultiSiteCheckQ12" name="PB Multi-Site Check Q12" camunda:decisionRef="Decision_core_info_multi_site_q12">
<bpmn:incoming>Flow_09h1imz</bpmn:incoming>

Binary file not shown.

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1wv9t3c" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1wv9t3c" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_19ej1y2" name="Data Securty Plan" isExecutable="true">
<bpmn:startEvent id="StartEvent_1co48s3">
<bpmn:outgoing>SequenceFlow_100w7co</bpmn:outgoing>
@ -10,45 +10,27 @@
<camunda:formData>
<camunda:formField id="HIPAA_Ids" label="HIPAA Identifiers" type="enum">
<camunda:properties>
<camunda:property id="group" value="hipaa" />
<camunda:property id="repeat" value="hipaa" />
<camunda:property id="spreadsheet.name" value="HIPAA_Ids.xls" />
<camunda:property id="spreadsheet.value.column" value="Value" />
<camunda:property id="spreadsheet.label.column" value="Label" />
<camunda:property id="repeat_button_label" value="Add HIPAA Id" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="HIPAA_Ids0" name="No HIPAA identifiers will be recorded as part of this research" />
<camunda:value id="HIPAA_Ids1" name="1. Name - Highly Sensitive Data " />
<camunda:value id="HIPAA_Ids2a" name="2a. Postal address includes street and/or PO Box, and town or city, state, and zip code - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids2b" name="2b. Postal address that includes only town or city, state, and/or zip code - Moderately Sensitive Data" />
<camunda:value id="HIPAA_Ids3" name="3. All date elements (except year) for dates related to an individual, e.g. service date" />
<camunda:value id="HIPAA_Ids4" name="4. Telephone numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids5" name="5. Fax numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids6" name="6. Electronic mail addresses - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids7" name="7. Social Security number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids8" name="8. Medical Record number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids9" name="9. Health plan beneficiary numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids10" name="10. Account numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids11" name="11. Certificate/license numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids12" name="12. Vehicle identifiers and serial numbers, including license plate numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids13" name="13. Device identifiers and serial numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids14" name="14. Web Universal Resource Locators (URLs) - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids15" name="15. Internet Protocol (IP) address numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids16" name="16. Biometric identifiers, including finger and voice prints - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids17" name="17. Full face photographic images and any comparable images - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids18" name="18. Other unique number, characteristic, code related to an individual, e.g. initials" />
</camunda:formField>
<camunda:formField id="HIPPA_IdsDataQualifiers" label="Check each that apply:" type="enum">
<camunda:formField id="HIPAA_IdsDataQualifiers" label="Check each that apply:" type="enum">
<camunda:properties>
<camunda:property id="enum_type" value="checkbox" />
<camunda:property id="group" value="hipaa" />
<camunda:property id="repeat" value="hipaa" />
<camunda:property id="hide_expression" value="console.log(&#39;this&#39;, this) || model.HIPAA_Ids === &#39;HIPAA_Ids0&#39;" />
<camunda:property id="hide_expression" value="model.HIPAA_Ids === &#39;HIPAA_Ids0&#39;" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="!(model.HIPAA_Ids === &#39;HIPAA_Ids0&#39;) || model.HIPAA_Ids == null" />
</camunda:validation>
<camunda:value id="OrigSource" name="Original source data collection (receive, collect, or record at UVa)" />
<camunda:value id="ShortTerm" name="Store long term at UVa" />
<camunda:value id="LongTerm" name="Store long term at UVa" />
<camunda:value id="SendTransOutside" name="Send or transmit outside of UVA" />
</camunda:formField>
</camunda:formData>
@ -162,28 +144,10 @@
<camunda:properties>
<camunda:property id="enum_type" value="checkbox" />
<camunda:property id="help" value="You may locate the server/drive name and path by taking the following steps:\n\n- Windows: Click your “computer icon”, right click on the Drive icon (e.g., F). Then click on ”properties”. The server/drive name and path will appear at the very top of the box.\n- If you need additional assistance, contact your department computer support or system administrator for assistance." />
<camunda:property id="spreadsheet.name" value="HIPAA_Ids.xls" />
<camunda:property id="spreadsheet.value.column" value="Value" />
<camunda:property id="spreadsheet.label.column" value="Label" />
</camunda:properties>
<camunda:value id="domatlas" name="domatlas.eservices.virginia.edu" />
<camunda:value id="dom-titan" name="dom-titan.eservices.virginia.edu" />
<camunda:value id="elson1" name="elson1.studenthealth.virginia.edu" />
<camunda:value id="es3" name="es3.eservices.virginia.edu" />
<camunda:value id="gcrcserver" name="gcrcserver.itc.virginia.edu" />
<camunda:value id="hscs-ss7" name="\\hscs-ss7" />
<camunda:value id="hscs-ss8" name="\\hscs-ss8" />
<camunda:value id="hscs-ss9" name="\\hscs-ss9" />
<camunda:value id="hscs-ss10" name="\\hscs-ss10" />
<camunda:value id="hscs-ss11" name="\\hscs-ss11" />
<camunda:value id="hscs-ss12" name="\\hscs-ss12" />
<camunda:value id="hscs-ss13" name="\\hscs-ss13" />
<camunda:value id="hscs-share1" name="\\hscs-share1\" />
<camunda:value id="hscs-share2" name="\\hscs-share2\" />
<camunda:value id="hscs-share3" name="\\hscs-share3\" />
<camunda:value id="radshare" name="\\radshare\" />
<camunda:value id="upgusers" name="upgusers.hscs.virginia.edu" />
<camunda:value id="Ivy" name="Ivy Secure Computing Platform/ Ivy Secure Cloud/Ivy Cloud" />
<camunda:value id="SECURE" name="School of Nursing SECURE NET" />
<camunda:value id="DropBoxSookasa" name="UVa HIT DropBox/Sookasa" />
<camunda:value id="Qualtrics" name=" UVa Qualtrics HSD survey tool: https://virginiahsd.co1.qualtrics.com/ControlPanel/" />
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
@ -202,31 +166,14 @@
<camunda:property id="hide_expression" value="!(model.CollStorUVaLocWebCloud)" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="CollStorUVaLocWebCloudHIPPA" label="Check all HIPAA Identifiers stored on a web based server, cloud server, and/or any non-centrally managed UVA server." type="enum">
<camunda:formField id="CollStorUVaLocWebCloudHIPAAIds" label="Check all HIPAA Identifiers stored on a web based server, cloud server, and/or any non-centrally managed UVA server." type="enum">
<camunda:properties>
<camunda:property id="enum_type" value="checkbox" />
<camunda:property id="hide_expression" value="!(model.CollStorUVaLocWebCloud)" />
<camunda:property id="spreadsheet.name" value="HIPAA_Ids.xls" />
<camunda:property id="spreadsheet.value.column" value="Value" />
<camunda:property id="spreadsheet.label.column" value="Label" />
</camunda:properties>
<camunda:value id="HIPAA_Ids0" name="No HIPAA identifiers will be recorded as part of this research" />
<camunda:value id="HIPAA_Ids1" name="1. Name - Highly Sensitive Data " />
<camunda:value id="HIPAA_Ids2a" name="2a. Postal address includes street and/or PO Box, and town or city, state, and zip code - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids2b" name="2b. Postal address that includes only town or city, state, and/or zip code - Moderately Sensitive Data" />
<camunda:value id="HIPAA_Ids3" name="3. All date elements (except year) for dates related to an individual, e.g. service date" />
<camunda:value id="HIPAA_Ids4" name="4. Telephone numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids5" name="5. Fax numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids6" name="6. Electronic mail addresses - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids7" name="7. Social Security number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids8" name="8. Medical Record number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids9" name="9. Health plan beneficiary numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids10" name="10. Account numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids11" name="11. Certificate/license numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids12" name="12. Vehicle identifiers and serial numbers, including license plate numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids13" name="13. Device identifiers and serial numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids14" name="14. Web Universal Resource Locators (URLs) - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids15" name="15. Internet Protocol (IP) address numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids16" name="16. Biometric identifiers, including finger and voice prints - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids17" name="17. Full face photographic images and any comparable images - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids18" name="18. Other unique number, characteristic, code related to an individual, e.g. initials" />
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
@ -253,9 +200,8 @@
<bpmn:documentation>Answer the questions for each of the Individual Use Devices that you use to collect or store your data onto your individual use device during the course of your research. Do not select these items if they are only to be used to connect elsewhere (to the items you identified in Electronic Medical Record, UVA approved eCRF or clinical trials management system, UVA servers &amp; websites, and Web-based server, cloud server, or any non-centrally managed server):</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IUD_IndividualUseDevices" label="Individual use devices:" type="enum">
<camunda:formField id="IUD" label="Individual use devices:" type="enum">
<camunda:properties>
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
</camunda:properties>
<camunda:validation>
@ -276,95 +222,75 @@
<camunda:value id="FitnessTrackers" name="Fitness Trackers" />
<camunda:value id="Other" name="Other" />
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesOther" label="Since you selected &#34;Other&#34; above, please identify the device type:" type="textarea">
<camunda:formField id="IUD_Other" label="Since you selected &#34;Other&#34; above, please identify the device type:" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices !== &#39;Other&#39;" />
<camunda:property id="hide_expression" value="model.IUD !== &#39;Other&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesProcess" label="Please describe your process for collecting, storing and/or transmitting data on the Individual Use Devices you selected in earlier steps (phones, flash drives, CDs, etc.):" type="textarea">
<camunda:formField id="IUD_Process" label="Please describe your process for collecting, storing and/or transmitting data on the Individual Use Devices you selected in earlier steps (phones, flash drives, CDs, etc.):" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices == &#39;None&#39;" />
<camunda:property id="hide_expression" value="model.IUD_Devices == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesHIPPA_Ids" label="Check the HIPAA Identifiers stored with the data on this device (e.g. such as full-face picture or video):" type="enum">
<camunda:formField id="IUD_HIPPA_Ids" label="Check the HIPAA Identifiers stored with the data on this device (e.g. such as full-face picture or video):" type="enum">
<camunda:properties>
<camunda:property id="enum_type" value="checkbox" />
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices == &#39;None&#39;" />
<camunda:property id="hide_expression" value="model.IUD == &#39;None&#39;" />
<camunda:property id="spreadsheet.name" value="HIPAA_Ids.xls" />
<camunda:property id="spreadsheet.value.column" value="Value" />
<camunda:property id="spreadsheet.label.column" value="Label" />
</camunda:properties>
<camunda:value id="HIPAA_Ids0" name="No HIPAA identifiers will be recorded as part of this research" />
<camunda:value id="HIPAA_Ids1" name="1. Name - Highly Sensitive Data " />
<camunda:value id="HIPAA_Ids2a" name="2a. Postal address includes street and/or PO Box, and town or city, state, and zip code - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids2b" name="2b. Postal address that includes only town or city, state, and/or zip code - Moderately Sensitive Data" />
<camunda:value id="HIPAA_Ids3" name="3. All date elements (except year) for dates related to an individual, e.g. service date" />
<camunda:value id="HIPAA_Ids4" name="4. Telephone numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids5" name="5. Fax numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids6" name="6. Electronic mail addresses - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids7" name="7. Social Security number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids8" name="8. Medical Record number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids9" name="9. Health plan beneficiary numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids10" name="10. Account numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids11" name="11. Certificate/license numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids12" name="12. Vehicle identifiers and serial numbers, including license plate numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids13" name="13. Device identifiers and serial numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids14" name="14. Web Universal Resource Locators (URLs) - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids15" name="15. Internet Protocol (IP) address numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids16" name="16. Biometric identifiers, including finger and voice prints - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids17" name="17. Full face photographic images and any comparable images - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids18" name="18. Other unique number, characteristic, code related to an individual, e.g. initials" />
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesBackups" label="Describe any backups made of the data stored on the device. Please include the location &#38; method of data transfer:" type="textarea">
<camunda:formField id="IUD_Backups" label="Describe any backups made of the data stored on the device. Please include the location &#38; method of data transfer:" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices == &#39;None&#39;" />
<camunda:property id="hide_expression" value="model.IUD == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesDelete" label="After the information is transferred elsewhere, will you securely delete all the data from this device?" type="boolean">
<camunda:formField id="IUD_HowLong" label="How long will the data remain on the individual-use device before being transferred?" type="textarea">
<camunda:properties>
<camunda:property id="group" value="devices" />
<camunda:property id="rows" value="5" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices == &#39;None&#39;" />
<camunda:property id="hide_expression" value="model.IUD == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesAccessYes" label="If yes, describe:" type="textarea">
<camunda:formField id="IUD_DeleteData" label="After the information is transferred elsewhere, will you securely delete all the data from this device?" type="boolean">
<camunda:properties>
<camunda:property id="repeat" value="devices" />
<camunda:property id="hide_expression" value="model.IUD == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_Access" label="Will anyone other than the study team or sponsor/CRO have access to data on this device?" type="boolean">
<camunda:properties>
<camunda:property id="repeat" value="devices" />
<camunda:property id="hide_expression" value="model.IUD == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_AccessYes" label="If yes, describe:" type="textarea">
<camunda:properties>
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="rows" value="5" />
<camunda:property id="hide_expression" value="!(model.IUD_IndividualUseDevicesDelete) || model.IUD_IndividualUseDevicesDelete == null || model.IUD_IndividualUseDevices == &#39;None&#39;" />
<camunda:property id="hide_expression" value="model.IUD === &#39;None&#39; || !model.IUD_Access" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesAccess" label="Will anyone other than the study team or sponsor/CRO have access to data on this device?" type="textarea">
<camunda:formField id="IUD_Alternatives" label="Other storage alternatives that were considered and the reasons they are unworkable:" type="textarea">
<camunda:properties>
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="rows" value="5" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices == &#39;None&#39;" />
<camunda:property id="hide_expression" value="model.IUD == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesAlternatives" label="Other storage alternatives that were considered and the reasons they are unworkable:" type="textarea">
<camunda:formField id="IUD_Justification" label="The justification for storage of these data on this individual use device is:" type="textarea">
<camunda:properties>
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="rows" value="5" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IUD_IndividualUseDevicesJustification" label="The justification for storage of these data on this individual use device is:" type="textarea">
<camunda:properties>
<camunda:property id="group" value="devices" />
<camunda:property id="repeat" value="devices" />
<camunda:property id="rows" value="5" />
<camunda:property id="hide_expression" value="model.IUD_IndividualUseDevices == &#39;None&#39;" />
<camunda:property id="hide_expression" value="model.IUD == &#39;None&#39;" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
@ -372,7 +298,7 @@
<bpmn:incoming>SequenceFlow_0nc6lcs</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0gp2pjm</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_0gp2pjm" sourceRef="Task_EnterIndividualUseDevices" targetRef="Activity_1qbfs1w" />
<bpmn:sequenceFlow id="SequenceFlow_0gp2pjm" sourceRef="Task_EnterIndividualUseDevices" targetRef="Task_EnterOutsideUVA" />
<bpmn:sequenceFlow id="SequenceFlow_0mgwas4" sourceRef="Task_EnterOutsideUVA" targetRef="ExclusiveGateway_0pi0c2d" />
<bpmn:sequenceFlow id="SequenceFlow_1i8e52t" sourceRef="ExclusiveGateway_0x3t2vl" targetRef="Task_EnterEmailMethods" />
<bpmn:userTask id="Task_EnterOutsideUVA" name="Enter Outside of UVA" camunda:formKey="EnterOutsideUVa">
@ -389,7 +315,7 @@ Indicate all the possible formats in which you will transmit your data outside o
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0cpwkms</bpmn:incoming>
<bpmn:incoming>SequenceFlow_0gp2pjm</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0mgwas4</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_EnterEmailMethods" name="Enter Email Methods" camunda:formKey="EnterEmailMethods">
@ -415,7 +341,6 @@ Indicate all the possible formats in which you will transmit your data outside o
<camunda:formField id="EnterDataManagementWebwsiteServerDriveAck" label="I acknowledge that ANY electronic individual use devices used to connect to any servers/websites listed below are supported by UVA Health System IT." type="boolean">
<camunda:properties>
<camunda:property id="help" value="[Definition of electronic individual user devices](https://security.virginia.edu/individual%E2%80%93use-electronic-devices)" />
<camunda:property id="group" value="DataSentSponsorCRO" />
<camunda:property id="repeat" value="DataSentSponsorCRO" />
</camunda:properties>
<camunda:validation>
@ -424,48 +349,27 @@ Indicate all the possible formats in which you will transmit your data outside o
</camunda:formField>
<camunda:formField id="EnterDataManagementHIPAA_Ids" label="HIPAA Identifiers" type="enum">
<camunda:properties>
<camunda:property id="group" value="DataSentSponsorCRO" />
<camunda:property id="repeat" value="DataSentSponsorCRO" />
<camunda:property id="spreadsheet.name" value="HIPAA_Ids.xls" />
<camunda:property id="spreadsheet.value.column" value="Value" />
<camunda:property id="spreadsheet.label.column" value="Label" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="HIPAA_Ids0" name="No HIPAA identifiers will be recorded as part of this research" />
<camunda:value id="HIPAA_Ids1" name="1. Name - Highly Sensitive Data " />
<camunda:value id="HIPAA_Ids2a" name="2a. Postal address includes street and/or PO Box, and town or city, state, and zip code - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids2b" name="2b. Postal address that includes only town or city, state, and/or zip code - Moderately Sensitive Data" />
<camunda:value id="HIPAA_Ids3" name="3. All date elements (except year) for dates related to an individual, e.g. service date" />
<camunda:value id="HIPAA_Ids4" name="4. Telephone numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids5" name="5. Fax numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids6" name="6. Electronic mail addresses - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids7" name="7. Social Security number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids8" name="8. Medical Record number - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids9" name="9. Health plan beneficiary numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids10" name="10. Account numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids11" name="11. Certificate/license numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids12" name="12. Vehicle identifiers and serial numbers, including license plate numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids13" name="13. Device identifiers and serial numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids14" name="14. Web Universal Resource Locators (URLs) - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids15" name="15. Internet Protocol (IP) address numbers - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids16" name="16. Biometric identifiers, including finger and voice prints - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids17" name="17. Full face photographic images and any comparable images - Highly Sensitive Data" />
<camunda:value id="HIPAA_Ids18" name="18. Other unique number, characteristic, code related to an individual, e.g. initials" />
</camunda:formField>
<camunda:formField id="EnterDataManagementSharingContract" label="If sharing data with anyone outside of UVA, do you confirm that you will obtain a contract with them via the School of Medicine Office of Grants and Contracts (OGC) or the Office of Sponsored Programs (OSP)?" type="boolean">
<camunda:properties>
<camunda:property id="group" value="DataSentSponsorCRO" />
<camunda:property id="repeat" value="DataSentSponsorCRO" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="EnterDataManagementWebsiteServerDriveEncrypted" label="Data will be sent and stored in an encrypted fashion (e.g. will only be shared and via Secure FX, Secure FTP, HTTPS, PGP) and the server/drive is configured to store data regulated by HIPAA" type="boolean">
<camunda:properties>
<camunda:property id="group" value="DataSentSponsorCRO" />
<camunda:property id="repeat" value="DataSentSponsorCRO" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="EnterDataManagementWebsiteServerDriveURL" label="Name (URL) of website (e.g. https://remote.sponsor.com/project name)" type="textarea">
<camunda:properties>
<camunda:property id="group" value="DataSentSponsorCRO" />
<camunda:property id="repeat" value="DataSentSponsorCRO" />
</camunda:properties>
</camunda:formField>
@ -498,7 +402,7 @@ Indicate all the possible formats in which you will transmit your data outside o
</camunda:formField>
<camunda:formField id="DataTransmissionMethodEncrypted" label="Individual Use Devices" type="enum">
<camunda:properties>
<camunda:property id="enum_type" value="checkbox" />
<camunda:property id="enum_type" value="radio" />
<camunda:property id="markdown_description" value="**Note:** Examples of individual use devices: CD, thumb drive, etc." />
</camunda:properties>
<camunda:value id="Yes" name="Yes, individual use devices will be shipped using a trackable method with data encrypted and password to the encrypted data transmitted separately" />
@ -506,7 +410,7 @@ Indicate all the possible formats in which you will transmit your data outside o
</camunda:formField>
<camunda:formField id="DataTransmissionMethodFaxed" label="Faxed" type="enum">
<camunda:properties>
<camunda:property id="enum_type" value="checkbox" />
<camunda:property id="enum_type" value="radio" />
<camunda:property id="markdown_description" value="**Note:** By checking this option, you are also confirming you will verify FAX numbers before faxing and use FAX cover sheet with a confidentiality statement." />
</camunda:properties>
<camunda:value id="Yes" name="Yes, data wile be faxed to a receiving machine in a restricted-access location with the intended recipient is clearly indicated, alerted to the pending transmission and available to pick up immediately." />
@ -518,6 +422,7 @@ Indicate all the possible formats in which you will transmit your data outside o
<bpmn:outgoing>SequenceFlow_0lere0k</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="EndEvent_151cj59">
<bpmn:documentation>Done message</bpmn:documentation>
<bpmn:incoming>SequenceFlow_16kyite</bpmn:incoming>
</bpmn:endEvent>
<bpmn:exclusiveGateway id="ExclusiveGateway_0pi0c2d" name="Outside of UVa?">
@ -548,7 +453,7 @@ Indicate all the possible formats in which you will transmit your data outside o
<bpmn:incoming>SequenceFlow_0k2r83n</bpmn:incoming>
<bpmn:incoming>SequenceFlow_0t6xl9i</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_16kyite</bpmn:outgoing>
<bpmn:script>CompleteTemplate NEW_DSP_template.docx Study_DataSecurityPlan</bpmn:script>
<bpmn:script>#! CompleteTemplate NEW_DSP_template.docx Study_DataSecurityPlan</bpmn:script>
</bpmn:scriptTask>
<bpmn:manualTask id="Task_0q6ir2l" name="View Instructions">
<bpmn:documentation>##### Instructions
@ -568,7 +473,10 @@ Process: The Study Team will answer the questions in this section to create the
How to The Data Security Plan is auto-generated based on your answers on this Step. You can save your information here and check the outcomes on the Data Security Plan Upload Step at any time.
Submit the step only when you are ready. After you "Submit" the step, the information will not be available for editing.</bpmn:documentation>
Submit the step only when you are ready. After you "Submit" the step, the information will not be available for editing.
# test</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="FormField_23d42bc" label="asdg" type="boolean" />
@ -623,339 +531,222 @@ Indicate all the possible formats in which you will collect or receive your orig
<bpmn:incoming>SequenceFlow_0blyor8</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1oq4w2h</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:task id="Activity_1qbfs1w" name="Enter Device&#10;Details">
<bpmn:incoming>SequenceFlow_0gp2pjm</bpmn:incoming>
<bpmn:outgoing>Flow_0cpwkms</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics />
</bpmn:task>
<bpmn:sequenceFlow id="Flow_0cpwkms" sourceRef="Activity_1qbfs1w" targetRef="Task_EnterOutsideUVA" />
<bpmn:textAnnotation id="TextAnnotation_190dbhy">
<bpmn:text>&gt; Instructions
o Hippa Instructions
o Hippa Indentifiers
o Vuew Definitions and Instructions
o Paper Documents
o Emailed to UVA Personnel
o EMC (EPIC)
o UVA Approvled eCRF
o UVA Servers
o Web or Cloud Server
o Individual Use Devices
o Device Details
0 Outside of UVA
o Outside of UVA?
     o Yes 
           o Email Methods
           o Data Management
           o Transmission Method
           o Generate DSP 
    o No
           o Generate DSP</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1nrg5es" sourceRef="Task_0q6ir2l" targetRef="TextAnnotation_190dbhy" />
<bpmn:textAnnotation id="TextAnnotation_0l6dohi">
<bpmn:text>*  Instructions
* Hippa Instructions
* Hippa Indentifiers
o Vuew Definitions and Instructions
&gt;&gt; Paper Documents
&gt; Emailed to UVA Personnel
&gt; EMC (EPIC)
&gt; UVA Approvled eCRF
&gt; UVA Servers
&gt; Web or Cloud Server
o Individual Use Devices
o Device Details
o Outside of UVA
o Outside of UVA?
     o Yes 
           o Email Methods
           o Data Management
           o Transmission Method
           o Generate DSP 
    o No
           o Generate DSP</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_0ykpfju" sourceRef="Task_EnterPaperDocuments" targetRef="TextAnnotation_0l6dohi" />
<bpmn:textAnnotation id="TextAnnotation_0tpe506">
<bpmn:text>* Instructions
* Hippa Instructions
* Hippa Indentifiers
* View Definitions and Instructions
* Paper Documents (Parallel creates spaces)
* Emailed to UVA Personnel
* EMC (EPIC)
* UVA Approvled eCRF
* UVA Servers
* Web or Cloud Server
* Individual Use Devices
o Device Details (MultiInstance Indents, Parallel creates spaces))
&gt; Desktop
&gt;&gt; Laptop
&gt; Cell Phone
&gt; Other
o Outside of UVA
o Outside of UVA?
     o Yes 
           o Email Methods
           o Data Management
           o Transmission Method
           o Generate DSP 
    o No
           o Generate DSP</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_01x19xx" sourceRef="Activity_1qbfs1w" targetRef="TextAnnotation_0tpe506" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_19ej1y2">
<bpmndi:BPMNShape id="TextAnnotation_0l6dohi_di" bpmnElement="TextAnnotation_0l6dohi">
<dc:Bounds x="1200" y="80" width="283.5273279352227" height="309.04183535762485" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_190dbhy_di" bpmnElement="TextAnnotation_190dbhy">
<dc:Bounds x="190" y="150" width="237.9175101214575" height="309.04183535762485" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_0tpe506_di" bpmnElement="TextAnnotation_0tpe506">
<dc:Bounds x="1240" y="820" width="370" height="442" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_16kyite_di" bpmnElement="SequenceFlow_16kyite">
<di:waypoint x="2240" y="660" />
<di:waypoint x="2322" y="660" />
<di:waypoint x="2240" y="390" />
<di:waypoint x="2322" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0t6xl9i_di" bpmnElement="SequenceFlow_0t6xl9i">
<di:waypoint x="1620" y="685" />
<di:waypoint x="1620" y="910" />
<di:waypoint x="2190" y="910" />
<di:waypoint x="2190" y="700" />
<di:waypoint x="1620" y="415" />
<di:waypoint x="1620" y="640" />
<di:waypoint x="2190" y="640" />
<di:waypoint x="2190" y="430" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0k2r83n_di" bpmnElement="SequenceFlow_0k2r83n">
<di:waypoint x="2075" y="660" />
<di:waypoint x="2140" y="660" />
<di:waypoint x="2075" y="390" />
<di:waypoint x="2140" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0blyor8_di" bpmnElement="SequenceFlow_0blyor8">
<di:waypoint x="665" y="660" />
<di:waypoint x="717" y="660" />
<di:waypoint x="660" y="390" />
<di:waypoint x="717" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0jyty9m_di" bpmnElement="SequenceFlow_0jyty9m">
<di:waypoint x="498" y="660" />
<di:waypoint x="565" y="660" />
<di:waypoint x="498" y="390" />
<di:waypoint x="560" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0m2op9s_di" bpmnElement="SequenceFlow_0m2op9s">
<di:waypoint x="351" y="660" />
<di:waypoint x="398" y="660" />
<di:waypoint x="351" y="390" />
<di:waypoint x="398" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1oq4w2h_di" bpmnElement="SequenceFlow_1oq4w2h">
<di:waypoint x="817" y="660" />
<di:waypoint x="875" y="660" />
<di:waypoint x="817" y="390" />
<di:waypoint x="875" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_100w7co_di" bpmnElement="SequenceFlow_100w7co">
<di:waypoint x="191" y="660" />
<di:waypoint x="251" y="660" />
<di:waypoint x="178" y="390" />
<di:waypoint x="251" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_01hl869_di" bpmnElement="SequenceFlow_01hl869">
<di:waypoint x="1645" y="660" />
<di:waypoint x="1725" y="660" />
<di:waypoint x="1645" y="390" />
<di:waypoint x="1725" y="390" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1676" y="642" width="19" height="14" />
<dc:Bounds x="1677" y="372" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0lere0k_di" bpmnElement="SequenceFlow_0lere0k">
<di:waypoint x="1950" y="800" />
<di:waypoint x="2050" y="800" />
<di:waypoint x="2050" y="685" />
<di:waypoint x="1950" y="530" />
<di:waypoint x="2050" y="530" />
<di:waypoint x="2050" y="415" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0uewki3_di" bpmnElement="SequenceFlow_0uewki3">
<di:waypoint x="1950" y="520" />
<di:waypoint x="2050" y="520" />
<di:waypoint x="2050" y="635" />
<di:waypoint x="1950" y="250" />
<di:waypoint x="2050" y="250" />
<di:waypoint x="2050" y="365" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_08rwbhm_di" bpmnElement="SequenceFlow_08rwbhm">
<di:waypoint x="1950" y="660" />
<di:waypoint x="2025" y="660" />
<di:waypoint x="1950" y="390" />
<di:waypoint x="2025" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1mnmo6p_di" bpmnElement="SequenceFlow_1mnmo6p">
<di:waypoint x="1750" y="685" />
<di:waypoint x="1750" y="800" />
<di:waypoint x="1850" y="800" />
<di:waypoint x="1750" y="415" />
<di:waypoint x="1750" y="530" />
<di:waypoint x="1850" y="530" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_12bv2i4_di" bpmnElement="SequenceFlow_12bv2i4">
<di:waypoint x="1775" y="660" />
<di:waypoint x="1850" y="660" />
<di:waypoint x="1775" y="390" />
<di:waypoint x="1850" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1i8e52t_di" bpmnElement="SequenceFlow_1i8e52t">
<di:waypoint x="1750" y="635" />
<di:waypoint x="1750" y="520" />
<di:waypoint x="1850" y="520" />
<di:waypoint x="1750" y="365" />
<di:waypoint x="1750" y="250" />
<di:waypoint x="1850" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0mgwas4_di" bpmnElement="SequenceFlow_0mgwas4">
<di:waypoint x="1570" y="660" />
<di:waypoint x="1595" y="660" />
<di:waypoint x="1530" y="390" />
<di:waypoint x="1595" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0gp2pjm_di" bpmnElement="SequenceFlow_0gp2pjm">
<di:waypoint x="1300" y="660" />
<di:waypoint x="1330" y="660" />
<di:waypoint x="1360" y="390" />
<di:waypoint x="1430" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0nc6lcs_di" bpmnElement="SequenceFlow_0nc6lcs">
<di:waypoint x="1185" y="660" />
<di:waypoint x="1200" y="660" />
<di:waypoint x="1185" y="390" />
<di:waypoint x="1260" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_10fsxk4_di" bpmnElement="SequenceFlow_10fsxk4">
<di:waypoint x="1080" y="910" />
<di:waypoint x="1160" y="910" />
<di:waypoint x="1160" y="685" />
<di:waypoint x="1080" y="640" />
<di:waypoint x="1160" y="640" />
<di:waypoint x="1160" y="415" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1xp62py_di" bpmnElement="SequenceFlow_1xp62py">
<di:waypoint x="1080" y="810" />
<di:waypoint x="1160" y="810" />
<di:waypoint x="1160" y="685" />
<di:waypoint x="1080" y="540" />
<di:waypoint x="1160" y="540" />
<di:waypoint x="1160" y="415" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1agmshr_di" bpmnElement="SequenceFlow_1agmshr">
<di:waypoint x="1080" y="710" />
<di:waypoint x="1160" y="710" />
<di:waypoint x="1160" y="685" />
<di:waypoint x="1080" y="440" />
<di:waypoint x="1160" y="440" />
<di:waypoint x="1160" y="415" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_12cos7w_di" bpmnElement="SequenceFlow_12cos7w">
<di:waypoint x="1080" y="490" />
<di:waypoint x="1160" y="490" />
<di:waypoint x="1160" y="635" />
<di:waypoint x="1080" y="220" />
<di:waypoint x="1160" y="220" />
<di:waypoint x="1160" y="365" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1q6gf6w_di" bpmnElement="SequenceFlow_1q6gf6w">
<di:waypoint x="1080" y="390" />
<di:waypoint x="1160" y="390" />
<di:waypoint x="1160" y="635" />
<di:waypoint x="1080" y="120" />
<di:waypoint x="1160" y="120" />
<di:waypoint x="1160" y="365" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0z10m1d_di" bpmnElement="SequenceFlow_0z10m1d">
<di:waypoint x="1080" y="590" />
<di:waypoint x="1160" y="590" />
<di:waypoint x="1160" y="635" />
<di:waypoint x="1080" y="320" />
<di:waypoint x="1160" y="320" />
<di:waypoint x="1160" y="365" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0obqjjx_di" bpmnElement="SequenceFlow_0obqjjx">
<di:waypoint x="900" y="685" />
<di:waypoint x="900" y="910" />
<di:waypoint x="980" y="910" />
<di:waypoint x="900" y="415" />
<di:waypoint x="900" y="640" />
<di:waypoint x="980" y="640" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0ng3fm8_di" bpmnElement="SequenceFlow_0ng3fm8">
<di:waypoint x="900" y="685" />
<di:waypoint x="900" y="810" />
<di:waypoint x="980" y="810" />
<di:waypoint x="900" y="415" />
<di:waypoint x="900" y="540" />
<di:waypoint x="980" y="540" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0pw57x9_di" bpmnElement="SequenceFlow_0pw57x9">
<di:waypoint x="900" y="685" />
<di:waypoint x="900" y="710" />
<di:waypoint x="980" y="710" />
<di:waypoint x="900" y="415" />
<di:waypoint x="900" y="440" />
<di:waypoint x="980" y="440" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_10g92nf_di" bpmnElement="SequenceFlow_10g92nf">
<di:waypoint x="900" y="635" />
<di:waypoint x="900" y="590" />
<di:waypoint x="980" y="590" />
<di:waypoint x="900" y="365" />
<di:waypoint x="900" y="320" />
<di:waypoint x="980" y="320" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_084dyht_di" bpmnElement="SequenceFlow_084dyht">
<di:waypoint x="900" y="635" />
<di:waypoint x="900" y="490" />
<di:waypoint x="980" y="490" />
<di:waypoint x="900" y="365" />
<di:waypoint x="900" y="220" />
<di:waypoint x="980" y="220" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1i6ac9a_di" bpmnElement="SequenceFlow_1i6ac9a">
<di:waypoint x="900" y="635" />
<di:waypoint x="900" y="390" />
<di:waypoint x="980" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0cpwkms_di" bpmnElement="Flow_0cpwkms">
<di:waypoint x="1430" y="660" />
<di:waypoint x="1470" y="660" />
<di:waypoint x="900" y="365" />
<di:waypoint x="900" y="120" />
<di:waypoint x="980" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="StartEvent_1co48s3_di" bpmnElement="StartEvent_1co48s3">
<dc:Bounds x="155" y="642" width="36" height="36" />
<dc:Bounds x="142" y="372" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_16imtaa_di" bpmnElement="Task_EnterHIPAAIdentifiers">
<dc:Bounds x="565" y="620" width="100" height="80" />
<dc:Bounds x="560" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ParallelGateway_03qblyb_di" bpmnElement="ExclusiveGateway_0b16kmf">
<dc:Bounds x="875" y="635" width="50" height="50" />
<dc:Bounds x="875" y="365" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1wsga3m_di" bpmnElement="Task_EnterPaperDocuments">
<dc:Bounds x="980" y="350" width="100" height="80" />
<dc:Bounds x="980" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0o1xjub_di" bpmnElement="Task_EnterEmailedUVAPersonnel">
<dc:Bounds x="980" y="450" width="100" height="80" />
<dc:Bounds x="980" y="180" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1gnbchf_di" bpmnElement="Task_EnterEMR">
<dc:Bounds x="980" y="550" width="100" height="80" />
<dc:Bounds x="980" y="280" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0a4bj92_di" bpmnElement="Task_EnterUVAApprovedECRF">
<dc:Bounds x="980" y="670" width="100" height="80" />
<dc:Bounds x="980" y="400" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1f2b80a_di" bpmnElement="Task_EnterUVaServersWebsites">
<dc:Bounds x="980" y="500" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0n3jbd7_di" bpmnElement="Task_EnterWebCloudServer">
<dc:Bounds x="980" y="600" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ParallelGateway_0zl5t7b_di" bpmnElement="ExclusiveGateway_06kvl84">
<dc:Bounds x="1135" y="635" width="50" height="50" />
<dc:Bounds x="1135" y="365" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0q8o038_di" bpmnElement="Task_EnterIndividualUseDevices">
<dc:Bounds x="1260" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_10x8kgc_di" bpmnElement="Task_EnterOutsideUVA">
<dc:Bounds x="1430" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1x8azom_di" bpmnElement="Task_EnterEmailMethods">
<dc:Bounds x="1850" y="480" width="100" height="80" />
<dc:Bounds x="1850" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1r640re_di" bpmnElement="Task_EnterDataManagement">
<dc:Bounds x="1850" y="620" width="100" height="80" />
<dc:Bounds x="1850" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ParallelGateway_0cignbh_di" bpmnElement="ExclusiveGateway_1lpm3pa">
<dc:Bounds x="2025" y="635" width="50" height="50" />
<dc:Bounds x="2025" y="365" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0ns9m8t_di" bpmnElement="Task_EnterTransmissionMethod">
<dc:Bounds x="1850" y="760" width="100" height="80" />
<dc:Bounds x="1850" y="490" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_151cj59_di" bpmnElement="EndEvent_151cj59">
<dc:Bounds x="2322" y="642" width="36" height="36" />
<dc:Bounds x="2322" y="372" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ExclusiveGateway_0pi0c2d_di" bpmnElement="ExclusiveGateway_0pi0c2d" isMarkerVisible="true">
<dc:Bounds x="1595" y="635" width="50" height="50" />
<dc:Bounds x="1595" y="365" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1580" y="611" width="80" height="14" />
<dc:Bounds x="1580" y="341" width="80" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ParallelGateway_1284xgu_di" bpmnElement="ExclusiveGateway_0x3t2vl">
<dc:Bounds x="1725" y="635" width="50" height="50" />
<dc:Bounds x="1725" y="365" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1616pnb_di" bpmnElement="Task_1ypw8ge">
<dc:Bounds x="2140" y="620" width="100" height="80" />
<dc:Bounds x="2140" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1l6rjbr_di" bpmnElement="Task_0q6ir2l">
<dc:Bounds x="251" y="620" width="100" height="80" />
<dc:Bounds x="251" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_11u7de2_di" bpmnElement="Task_0uotpzg">
<dc:Bounds x="398" y="620" width="100" height="80" />
<dc:Bounds x="398" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0nfmn0k_di" bpmnElement="Task_196zozc">
<dc:Bounds x="717" y="620" width="100" height="80" />
<dc:Bounds x="717" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0q8o038_di" bpmnElement="Task_EnterIndividualUseDevices">
<dc:Bounds x="1200" y="620" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_10x8kgc_di" bpmnElement="Task_EnterOutsideUVA">
<dc:Bounds x="1470" y="620" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1qbfs1w_di" bpmnElement="Activity_1qbfs1w">
<dc:Bounds x="1330" y="620" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1f2b80a_di" bpmnElement="Task_EnterUVaServersWebsites">
<dc:Bounds x="980" y="770" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0n3jbd7_di" bpmnElement="Task_EnterWebCloudServer">
<dc:Bounds x="980" y="870" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_0ykpfju_di" bpmnElement="Association_0ykpfju">
<di:waypoint x="1080" y="365" />
<di:waypoint x="1200" y="306" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1nrg5es_di" bpmnElement="Association_1nrg5es">
<di:waypoint x="302" y="620" />
<di:waypoint x="306" y="459" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_01x19xx_di" bpmnElement="Association_01x19xx">
<di:waypoint x="1385" y="700" />
<di:waypoint x="1399" y="820" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -41,8 +41,7 @@
{%- else -%}
| {{doc.display_name}} | Not started | [?](/help/documents/{{doc.code}}) | No file yet |
{%- endif %}
{% endif %}{% endfor %}
</bpmn:documentation>
{% endif %}{% endfor %}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="display_name" value="Documents and Approvals" />
@ -54,12 +53,12 @@
<bpmn:scriptTask id="Activity_0a14x7j" name="Load Approvals">
<bpmn:incoming>Flow_0c7ryff</bpmn:incoming>
<bpmn:outgoing>Flow_142jtxs</bpmn:outgoing>
<bpmn:script>StudyInfo approvals</bpmn:script>
<bpmn:script>#! StudyInfo approvals</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_1aju60t" name="Load Documents">
<bpmn:incoming>Flow_1k3su2q</bpmn:incoming>
<bpmn:outgoing>Flow_0c7ryff</bpmn:outgoing>
<bpmn:script>StudyInfo documents</bpmn:script>
<bpmn:script>#! StudyInfo documents</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_142jtxs" sourceRef="Activity_0a14x7j" targetRef="Activity_DisplayDocsAndApprovals" />
<bpmn:sequenceFlow id="Flow_0c7ryff" sourceRef="Activity_1aju60t" targetRef="Activity_0a14x7j" />

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1e7871f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1e7871f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_04jm0bm" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1dhb8f4</bpmn:outgoing>
@ -36,7 +36,7 @@
<bpmn:scriptTask id="ScriptTask_1fn00ox" name="Load IRB Details">
<bpmn:incoming>SequenceFlow_1dhb8f4</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1uzcl1f</bpmn:outgoing>
<bpmn:script>StudyInfo details</bpmn:script>
<bpmn:script>#! StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1uzcl1f" sourceRef="ScriptTask_1fn00ox" targetRef="Task_SupplementIDE" />
<bpmn:exclusiveGateway id="ExclusiveGateway_1fib89p" name="IS_IDE = True and Number Provided?&#10;&#10;">

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_07f7kut" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_07f7kut" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_IDS" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1dexemq</bpmn:outgoing>
@ -100,7 +100,7 @@ Protocol Owner: **(need to insert value here)**</bpmn:documentation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="FormField_Explain" label="Please explain" type="text_area">
<camunda:formField id="FormField_Explain" label="Please explain" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
</camunda:properties>
@ -123,7 +123,7 @@ Protocol Owner: **(need to insert value here)**</bpmn:documentation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="FormField_Explain" label="Please explain" type="text_area">
<camunda:formField id="FormField_Explain" label="Please explain" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
</camunda:properties>
@ -159,13 +159,13 @@ Protocol Owner: **(need to insert value here)**</bpmn:documentation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="FormField_Explain" label="Please explain" type="text_area">
<camunda:formField id="FormField_Explain" label="Please explain" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="FormField_Training" label="Is any training required prior to obtaining system access?" type="boolean" />
<camunda:formField id="FormField_Details" label="If yes, provide details:" type="text_area">
<camunda:formField id="FormField_Details" label="If yes, provide details:" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
<camunda:property id="hide_expression" value="!model.FormField_Training | model.FormField_Training == null" />
@ -206,7 +206,7 @@ Protocol Owner: **(need to insert value here)**</bpmn:documentation>
<camunda:value id="no" name="No" />
<camunda:value id="unknown" name="Unknown" />
</camunda:formField>
<camunda:formField id="FormField_Details" label="If yes or unknown, provide details:" type="text_area" />
<camunda:formField id="FormField_Details" label="If yes or unknown, provide details:" type="textarea" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0lixqzs</bpmn:incoming>
@ -217,128 +217,128 @@ Protocol Owner: **(need to insert value here)**</bpmn:documentation>
<bpmn:scriptTask id="Activity_LoadDocuments" name="Load Documents">
<bpmn:incoming>SequenceFlow_1dexemq</bpmn:incoming>
<bpmn:outgoing>Flow_1x9d2mo</bpmn:outgoing>
<bpmn:script>StudyInfo documents</bpmn:script>
<bpmn:script>#! StudyInfo documents</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_IDS">
<bpmndi:BPMNEdge id="Flow_1x9d2mo_di" bpmnElement="Flow_1x9d2mo">
<di:waypoint x="200" y="340" />
<di:waypoint x="270" y="340" />
<di:waypoint x="340" y="340" />
<di:waypoint x="410" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1r7kcks_di" bpmnElement="SequenceFlow_1r7kcks">
<di:waypoint x="1180" y="340" />
<di:waypoint x="1272" y="340" />
<di:waypoint x="1320" y="340" />
<di:waypoint x="1412" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0lixqzs_di" bpmnElement="SequenceFlow_0lixqzs">
<di:waypoint x="985" y="340" />
<di:waypoint x="1080" y="340" />
<di:waypoint x="1125" y="340" />
<di:waypoint x="1220" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1dexemq_di" bpmnElement="SequenceFlow_1dexemq">
<di:waypoint x="48" y="340" />
<di:waypoint x="100" y="340" />
<di:waypoint x="188" y="340" />
<di:waypoint x="240" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1bkjyhx_di" bpmnElement="SequenceFlow_1bkjyhx">
<di:waypoint x="645" y="340" />
<di:waypoint x="695" y="340" />
<di:waypoint x="785" y="340" />
<di:waypoint x="835" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1kam5in_di" bpmnElement="SequenceFlow_1kam5in">
<di:waypoint x="506" y="340" />
<di:waypoint x="545" y="340" />
<di:waypoint x="646" y="340" />
<di:waypoint x="685" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1dcu8zu_di" bpmnElement="SequenceFlow_1dcu8zu">
<di:waypoint x="370" y="340" />
<di:waypoint x="406" y="340" />
<di:waypoint x="510" y="340" />
<di:waypoint x="546" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0m01j99_di" bpmnElement="SequenceFlow_0m01j99">
<di:waypoint x="890" y="560" />
<di:waypoint x="960" y="560" />
<di:waypoint x="960" y="365" />
<di:waypoint x="1030" y="560" />
<di:waypoint x="1100" y="560" />
<di:waypoint x="1100" y="365" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1lys0jq_di" bpmnElement="SequenceFlow_1lys0jq">
<di:waypoint x="720" y="365" />
<di:waypoint x="720" y="560" />
<di:waypoint x="790" y="560" />
<di:waypoint x="860" y="365" />
<di:waypoint x="860" y="560" />
<di:waypoint x="930" y="560" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_13fzv9y_di" bpmnElement="SequenceFlow_13fzv9y">
<di:waypoint x="890" y="450" />
<di:waypoint x="960" y="450" />
<di:waypoint x="960" y="365" />
<di:waypoint x="1030" y="450" />
<di:waypoint x="1100" y="450" />
<di:waypoint x="1100" y="365" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0jwnfzy_di" bpmnElement="SequenceFlow_0jwnfzy">
<di:waypoint x="720" y="365" />
<di:waypoint x="720" y="450" />
<di:waypoint x="790" y="450" />
<di:waypoint x="860" y="365" />
<di:waypoint x="860" y="450" />
<di:waypoint x="930" y="450" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1pg0dkw_di" bpmnElement="SequenceFlow_1pg0dkw">
<di:waypoint x="890" y="340" />
<di:waypoint x="935" y="340" />
<di:waypoint x="1030" y="340" />
<di:waypoint x="1075" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0y21euo_di" bpmnElement="SequenceFlow_0y21euo">
<di:waypoint x="745" y="340" />
<di:waypoint x="790" y="340" />
<di:waypoint x="885" y="340" />
<di:waypoint x="930" y="340" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1iiazgn_di" bpmnElement="SequenceFlow_1iiazgn">
<di:waypoint x="890" y="120" />
<di:waypoint x="960" y="120" />
<di:waypoint x="960" y="315" />
<di:waypoint x="1030" y="120" />
<di:waypoint x="1100" y="120" />
<di:waypoint x="1100" y="315" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_100vc9e_di" bpmnElement="SequenceFlow_100vc9e">
<di:waypoint x="890" y="230" />
<di:waypoint x="960" y="230" />
<di:waypoint x="960" y="315" />
<di:waypoint x="1030" y="230" />
<di:waypoint x="1100" y="230" />
<di:waypoint x="1100" y="315" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0movigc_di" bpmnElement="SequenceFlow_0movigc">
<di:waypoint x="720" y="315" />
<di:waypoint x="720" y="230" />
<di:waypoint x="790" y="230" />
<di:waypoint x="860" y="315" />
<di:waypoint x="860" y="230" />
<di:waypoint x="930" y="230" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1guaev4_di" bpmnElement="SequenceFlow_1guaev4">
<di:waypoint x="720" y="315" />
<di:waypoint x="720" y="120" />
<di:waypoint x="790" y="120" />
<di:waypoint x="860" y="315" />
<di:waypoint x="860" y="120" />
<di:waypoint x="930" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="12" y="322" width="36" height="36" />
<dc:Bounds x="152" y="322" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0wr3vp4_di" bpmnElement="Task_EnterIDSStudyIdentification">
<dc:Bounds x="790" y="80" width="100" height="80" />
<dc:Bounds x="930" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0s34owg_di" bpmnElement="Task_EnterIDSFeeStructure">
<dc:Bounds x="790" y="190" width="100" height="80" />
<dc:Bounds x="930" y="190" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ParallelGateway_1c14ymx_di" bpmnElement="ExclusiveGateway_14igy57">
<dc:Bounds x="695" y="315" width="50" height="50" />
<dc:Bounds x="835" y="315" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ParallelGateway_188sr3c_di" bpmnElement="ExclusiveGateway_1b69uum">
<dc:Bounds x="935" y="315" width="50" height="50" />
<dc:Bounds x="1075" y="315" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0jypqha_di" bpmnElement="EndEvent_0jypqha">
<dc:Bounds x="1272" y="322" width="36" height="36" />
<dc:Bounds x="1412" y="322" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0li1vo4_di" bpmnElement="Task_ReviewPharmacyManualStatus">
<dc:Bounds x="790" y="300" width="100" height="80" />
<dc:Bounds x="930" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0uvz4r8_di" bpmnElement="UserTask_ReviewInvestigatorsBrochureStatus">
<dc:Bounds x="790" y="410" width="100" height="80" />
<dc:Bounds x="930" y="410" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_06sfx4u_di" bpmnElement="UserTask_ReviewIVRS-IWRS-IXRSManualStatus">
<dc:Bounds x="790" y="520" width="100" height="80" />
<dc:Bounds x="930" y="520" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_1ld7tdu_di" bpmnElement="BusinessRuleTask_PharmacyManual">
<dc:Bounds x="406" y="300" width="100" height="80" />
<dc:Bounds x="546" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_04d0y1w_di" bpmnElement="BusinessRuleTask_InvestigatorsBrochure">
<dc:Bounds x="270" y="300" width="100" height="80" />
<dc:Bounds x="410" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_03zh0rt_di" bpmnElement="BusinessRuleTask_IVRS-IWRS-IXRSManual">
<dc:Bounds x="545" y="300" width="100" height="80" />
<dc:Bounds x="685" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_03o04d0_di" bpmnElement="UserTask_03o04d0">
<dc:Bounds x="1080" y="300" width="100" height="80" />
<dc:Bounds x="1220" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0f5ox7w_di" bpmnElement="Activity_LoadDocuments">
<dc:Bounds x="100" y="300" width="100" height="80" />
<dc:Bounds x="240" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_0o0ff2r" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<decision id="decision_ind_check" name="IND Check">
<decisionTable id="decisionTable_1">
<input id="input_1" label="IS_IND">
<inputExpression id="inputExpression_1" typeRef="integer">
<text>StudyInfo.details.IS_IND</text>
</inputExpression>
</input>
<input id="InputClause_1yk6kx1" label="IND_1 Number?">
<inputExpression id="LiteralExpression_00xhtjw" typeRef="string">
<text>StudyInfo.details.IND_1</text>
</inputExpression>
</input>
<input id="InputClause_069sith" label="IND_2 Number?">
<inputExpression id="LiteralExpression_1h9kd8o" typeRef="string">
<text>StudyInfo.details.IND_2</text>
</inputExpression>
</input>
<input id="InputClause_0d0vpur" label="IND_3 Number?">
<inputExpression id="LiteralExpression_0zbsg01" typeRef="string">
<text>StudyInfo.details.IND_3</text>
</inputExpression>
</input>
<output id="output_1" label="Add Supplemental Data" name="ind_supplement" typeRef="boolean" />
<rule id="DecisionRule_0h0od2e">
<inputEntry id="UnaryTests_09ctq71">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1cub5pk">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0aubvru">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0rjeqez">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1we3duh">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_199dgpt">
<inputEntry id="UnaryTests_1ec0msc">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0h3sj7g">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_1ji4kgh">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_10gxrx9">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1fhlpya">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0teanii">
<inputEntry id="UnaryTests_0akfjdp">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1c88e2t">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0zfrdlt">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_07drghr">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_1i7dtia">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0m9aydp">
<inputEntry id="UnaryTests_003n37j">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_1fcaod2">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0hmnsvb">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0y6xian">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1wuhxz7">
<text>false</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -1,127 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1e7871f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_04jm0bm" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1dhb8f4</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1dhb8f4" sourceRef="StartEvent_1" targetRef="ScriptTask_1fn00ox" />
<bpmn:endEvent id="EndEvent_1h89sl4">
<bpmn:incoming>SequenceFlow_1yhv1qz</bpmn:incoming>
<bpmn:incoming>SequenceFlow_1enco3g</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="ScriptTask_1fn00ox" name="Load IRB Details">
<bpmn:incoming>SequenceFlow_1dhb8f4</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1uzcl1f</bpmn:outgoing>
<bpmn:script>StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1uzcl1f" sourceRef="ScriptTask_1fn00ox" targetRef="Task_SupplementIDE" />
<bpmn:exclusiveGateway id="ExclusiveGateway_1fib89p">
<bpmn:incoming>SequenceFlow_1lazou8</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1yb1vma</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_011l5xt</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="SequenceFlow_1yb1vma" name="Yes" sourceRef="ExclusiveGateway_1fib89p" targetRef="UserTask_0a2dfa8">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">ind_supplement == True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="SequenceFlow_011l5xt" name="No" sourceRef="ExclusiveGateway_1fib89p" targetRef="Task_NoIDE">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">ind_supplement == False</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:manualTask id="Task_NoIDE" name="IND But No Numbers">
<bpmn:documentation>The use of an Investigational New Drug (IND) was indicated in Protocol Builder, but no IND number was entered. Please enter up to three numbers in the Supplemental section of Protocol Builder so supplemental information can be entered here.</bpmn:documentation>
<bpmn:incoming>SequenceFlow_011l5xt</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1yhv1qz</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="SequenceFlow_1lazou8" sourceRef="Task_SupplementIDE" targetRef="ExclusiveGateway_1fib89p" />
<bpmn:businessRuleTask id="Task_SupplementIDE" name="Supplement IND?" camunda:decisionRef="decision_ind_check">
<bpmn:incoming>SequenceFlow_1uzcl1f</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1lazou8</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:sequenceFlow id="SequenceFlow_1yhv1qz" sourceRef="Task_NoIDE" targetRef="EndEvent_1h89sl4" />
<bpmn:userTask id="UserTask_0a2dfa8" name="Edit IND Info" camunda:formKey="FormKey_Details">
<bpmn:documentation>IND No.: {{StudyInfo.details.IND_1}}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="HolderType" label="IND Holder Type" type="enum">
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="HolderNameNotInList" label="IND Holder Name if not in above list" type="string" />
<camunda:formField id="DrugBiologicName" label="Drug/Biologic Name" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1yb1vma</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1enco3g</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1enco3g" sourceRef="UserTask_0a2dfa8" targetRef="EndEvent_1h89sl4" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_04jm0bm">
<bpmndi:BPMNEdge id="SequenceFlow_1enco3g_di" bpmnElement="SequenceFlow_1enco3g">
<di:waypoint x="810" y="117" />
<di:waypoint x="932" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1yhv1qz_di" bpmnElement="SequenceFlow_1yhv1qz">
<di:waypoint x="810" y="230" />
<di:waypoint x="871" y="230" />
<di:waypoint x="871" y="117" />
<di:waypoint x="932" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1lazou8_di" bpmnElement="SequenceFlow_1lazou8">
<di:waypoint x="510" y="117" />
<di:waypoint x="585" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_011l5xt_di" bpmnElement="SequenceFlow_011l5xt">
<di:waypoint x="610" y="142" />
<di:waypoint x="610" y="230" />
<di:waypoint x="710" y="230" />
<bpmndi:BPMNLabel>
<dc:Bounds x="618" y="183" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1yb1vma_di" bpmnElement="SequenceFlow_1yb1vma">
<di:waypoint x="635" y="117" />
<di:waypoint x="710" y="117" />
<bpmndi:BPMNLabel>
<dc:Bounds x="659" y="99" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1uzcl1f_di" bpmnElement="SequenceFlow_1uzcl1f">
<di:waypoint x="360" y="117" />
<di:waypoint x="410" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1dhb8f4_di" bpmnElement="SequenceFlow_1dhb8f4">
<di:waypoint x="188" y="117" />
<di:waypoint x="260" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1h89sl4_di" bpmnElement="EndEvent_1h89sl4">
<dc:Bounds x="932" y="99" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="414" y="202" width="74" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1fn00ox_di" bpmnElement="ScriptTask_1fn00ox">
<dc:Bounds x="260" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ExclusiveGateway_1fib89p_di" bpmnElement="ExclusiveGateway_1fib89p" isMarkerVisible="true">
<dc:Bounds x="585" y="92" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ManualTask_1f7z9wm_di" bpmnElement="Task_NoIDE">
<dc:Bounds x="710" y="190" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_1cszgkx_di" bpmnElement="Task_SupplementIDE">
<dc:Bounds x="410" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0a2dfa8_di" bpmnElement="UserTask_0a2dfa8">
<dc:Bounds x="710" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

Binary file not shown.

View File

@ -0,0 +1,220 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_0o0ff2r" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<decision id="decision_ind_check" name="IND Check">
<decisionTable id="decisionTable_1">
<input id="input_1" label="IS_IND">
<inputExpression id="inputExpression_1" typeRef="integer">
<text>StudyInfo.details.IS_IND</text>
</inputExpression>
</input>
<input id="InputClause_1yk6kx1" label="IND_1 Number?">
<inputExpression id="LiteralExpression_00xhtjw" typeRef="string">
<text>StudyInfo.details.IND_1</text>
</inputExpression>
</input>
<input id="InputClause_069sith" label="IND_2 Number?">
<inputExpression id="LiteralExpression_1h9kd8o" typeRef="string">
<text>StudyInfo.details.IND_2</text>
</inputExpression>
</input>
<input id="InputClause_0d0vpur" label="IND_3 Number?">
<inputExpression id="LiteralExpression_0zbsg01" typeRef="string">
<text>StudyInfo.details.IND_3</text>
</inputExpression>
</input>
<output id="output_1" label="Add Supplemental Data" name="ind_supplement" typeRef="boolean" />
<output id="OutputClause_0cfn42v" label="IND Count Entered" name="ind_cnt" typeRef="string" />
<output id="OutputClause_0xcdkqm" label="IND Message" name="ind_message" typeRef="string" />
<output id="OutputClause_08qk83g" label="IND 1 Field Value" name="IND1_Number" typeRef="string" />
<rule id="DecisionRule_0teanii">
<description>3 IND #s</description>
<inputEntry id="UnaryTests_0akfjdp">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1c88e2t">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0zfrdlt">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_07drghr">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_1i7dtia">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_0kulwlr">
<text>"three"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1tw8tzn">
<text>"Three IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1fiijih">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_199dgpt">
<description>2 IND #s</description>
<inputEntry id="UnaryTests_1ec0msc">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0h3sj7g">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_1ji4kgh">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_10gxrx9">
<text>""</text>
</inputEntry>
<outputEntry id="LiteralExpression_1fhlpya">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_1h5mox1">
<text>"two"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1nvcjhv">
<text>"Two IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1rwd1ja">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_0z0tcm0">
<description>3 IND#s, missing #2</description>
<inputEntry id="UnaryTests_1kf86r3">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0jm1wzq">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_14itgac">
<text>""</text>
</inputEntry>
<inputEntry id="UnaryTests_1prht5p">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_0pooubu">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_0nioovi">
<text>"two"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1fa5e2o">
<text>"Two IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1qul3vr">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_0bwkqh7">
<description>3 IND#s, missing #1</description>
<inputEntry id="UnaryTests_13ig4fh">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_11kb6cw">
<text>""</text>
</inputEntry>
<inputEntry id="UnaryTests_0sfwtwo">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0xxmh5j">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_14otjle">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_13qodmm">
<text>"two"</text>
</outputEntry>
<outputEntry id="LiteralExpression_0xhjgjn">
<text>"Two IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_13g0u0n">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_0h0od2e">
<description>1 IND #</description>
<inputEntry id="UnaryTests_09ctq71">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1cub5pk">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0aubvru">
<text>""</text>
</inputEntry>
<inputEntry id="UnaryTests_0rjeqez">
<text>""</text>
</inputEntry>
<outputEntry id="LiteralExpression_1we3duh">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_1jv0san">
<text>"one"</text>
</outputEntry>
<outputEntry id="LiteralExpression_19cvvhd">
<text>"One IND # entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_15ikz7u">
<text>StudyInfo.details.IND_1</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1nitohs">
<description>No</description>
<inputEntry id="UnaryTests_19oot48">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0i2qyga">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_09wye05">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_1g4y2ti">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_0c2mi3l">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_1e2kzvw">
<text>"na"</text>
</outputEntry>
<outputEntry id="LiteralExpression_0wj4zzb">
<text>"No IND Numbers Entered in PB"</text>
</outputEntry>
<outputEntry id="LiteralExpression_049iioi">
<text>""</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0m9aydp">
<description>No IND, PB Q#56 answered as No, should not be needed, but here as stopgap in case memu check failed</description>
<inputEntry id="UnaryTests_003n37j">
<text>0</text>
</inputEntry>
<inputEntry id="UnaryTests_1fcaod2">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0hmnsvb">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0y6xian">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1wuhxz7">
<text>false</text>
</outputEntry>
<outputEntry id="LiteralExpression_1dznftw">
<text></text>
</outputEntry>
<outputEntry id="LiteralExpression_1lbt5oy">
<text></text>
</outputEntry>
<outputEntry id="LiteralExpression_0tkt63s">
<text></text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1e7871f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.0.0">
<bpmn:process id="Process_04jm0bm" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1dhb8f4</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1dhb8f4" sourceRef="StartEvent_1" targetRef="ScriptTask_LoadIRBDetails" />
<bpmn:endEvent id="EndEvent_1h89sl4">
<bpmn:incoming>Flow_0jqdolk</bpmn:incoming>
<bpmn:incoming>Flow_OneOnly</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="ScriptTask_LoadIRBDetails" name="Load IRB Details">
<bpmn:incoming>SequenceFlow_1dhb8f4</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1uzcl1f</bpmn:outgoing>
<bpmn:script>#! StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1uzcl1f" sourceRef="ScriptTask_LoadIRBDetails" targetRef="Task_SupplementIDE" />
<bpmn:businessRuleTask id="Task_SupplementIDE" name="Current IND Status" camunda:decisionRef="decision_ind_check">
<bpmn:incoming>SequenceFlow_1uzcl1f</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1cwibmt</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:userTask id="IND_n1_info" name="Edit IND #1 Info" camunda:formKey="IND1_Info">
<bpmn:documentation>IND No.: {{ StudyInfo.details.IND_1 }}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND1_Number" label="IND1 number:" type="string">
<camunda:properties>
<camunda:property id="description" value="Enter IND number, if available." />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND1_HolderType" label="IND Holder Type" type="enum">
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="IND1_HolderName" label="Holder Name" type="autocomplete">
<camunda:properties>
<camunda:property id="spreadsheet.name" value="SponsorList.xls" />
<camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND1_HolderNameNotInList" label="IND Holder Name if not in above list" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="model.IND1_HolderName &#38;&#38; model.IND1_HolderName.value !== &#34;0&#34;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND1_DrugBiologicName" label="Drug/Biologic Name" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1bn0jp7</bpmn:incoming>
<bpmn:outgoing>Flow_10rb7gb</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="IND_n2_info" name="Edit IND #2 Info" camunda:formKey="IND2_Info">
<bpmn:documentation>IND No.:</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND2_Status" label="Do you have a second Investigational New Drug?" type="enum">
<camunda:value id="Yes" name="Yes" />
<camunda:value id="YesBut" name="Yes, but number is not available at this time" />
<camunda:value id="No" name="No" />
</camunda:formField>
<camunda:formField id="IND2_Number" label="IND2 Number:" type="string">
<camunda:properties>
<camunda:property id="value_expression" value="model.StudyInfo.details.IND_2" />
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND2_HolderType" label="IND Holder Type" type="enum">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="IND2_HolderName" label="Holder Name" type="autocomplete">
<camunda:properties>
<camunda:property id="spreadsheet.name" value="SponsorList.xls" />
<camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND2_HolderNameNotInList" label="IND Holder Name if not in above list" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39; || model.IND2_HolderName.value !== &#34;0&#34;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND2_DrugBiologicName" label="Drug/Biologic Name" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_TwoOrThree</bpmn:incoming>
<bpmn:outgoing>Flow_1p563xr</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="IND_n3_info" name="Edit IND #3 Info" camunda:formKey="IND3_Info">
<bpmn:documentation>IND No.:</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND3_Status" label="Do you have a third Investigational New Drug?" type="enum" defaultValue="No">
<camunda:value id="Yes" name="Yes" />
<camunda:value id="YesBut" name="Yes, but number is not available at this time." />
<camunda:value id="No" name="No" />
</camunda:formField>
<camunda:formField id="IND3_Number" label="IND3 Number:" type="string">
<camunda:properties>
<camunda:property id="value_expression" value="model.StudyInfo.details.IND_3" />
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND3_HolderType" label="IND Holder Type" type="enum">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="IND3_HolderName" label="Holder Name" type="autocomplete">
<camunda:properties>
<camunda:property id="spreadsheet.name" value="SponsorList.xls" />
<camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND3_HolderNameNotInList" label="IND Holder Name if not in above list" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39; || model.IND3_HolderName.value !== &#34;0&#34;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND3_DrugBiologicName" label="Drug/Biologic Name" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1p563xr</bpmn:incoming>
<bpmn:outgoing>Flow_0jqdolk</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1cwibmt" sourceRef="Task_SupplementIDE" targetRef="Activity_0yf2ypo" />
<bpmn:userTask id="Activity_0yf2ypo" name="Provide IND Count" camunda:formKey="IND_Count">
<bpmn:documentation>{{ ind_message }}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND_CntEntered" label="How Many?" type="enum" defaultValue="one">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="value_one" name="1 IND number" />
<camunda:value id="value_two" name="2 IND number" />
<camunda:value id="value_three" name="3 IND number" />
<camunda:value id="value_na" name="No IND Numbers in PB" />
</camunda:formField>
<camunda:formField id="FormField_0h8vmid" label="Test" type="string">
<camunda:properties>
<camunda:property id="value_expression" value="model.ind_cnt" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1cwibmt</bpmn:incoming>
<bpmn:outgoing>Flow_1bn0jp7</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1bn0jp7" sourceRef="Activity_0yf2ypo" targetRef="IND_n1_info" />
<bpmn:sequenceFlow id="Flow_1p563xr" sourceRef="IND_n2_info" targetRef="IND_n3_info" />
<bpmn:sequenceFlow id="Flow_0jqdolk" sourceRef="IND_n3_info" targetRef="EndEvent_1h89sl4" />
<bpmn:sequenceFlow id="Flow_10rb7gb" sourceRef="IND_n1_info" targetRef="Gateway_0ckycp9" />
<bpmn:exclusiveGateway id="Gateway_0ckycp9">
<bpmn:incoming>Flow_10rb7gb</bpmn:incoming>
<bpmn:outgoing>Flow_TwoOrThree</bpmn:outgoing>
<bpmn:outgoing>Flow_OneOnly</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_TwoOrThree" name="Two or Three INDs" sourceRef="Gateway_0ckycp9" targetRef="IND_n2_info">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">IND_CntEntered != "value_one"</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_OneOnly" name="One IND" sourceRef="Gateway_0ckycp9" targetRef="EndEvent_1h89sl4">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">IND_CntEntered == "value_one"</bpmn:conditionExpression>
</bpmn:sequenceFlow>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_04jm0bm">
<bpmndi:BPMNEdge id="Flow_00n2n7p_di" bpmnElement="Flow_OneOnly">
<di:waypoint x="940" y="142" />
<di:waypoint x="940" y="260" />
<di:waypoint x="1510" y="260" />
<di:waypoint x="1510" y="135" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1205" y="242" width="43" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1o2u7k3_di" bpmnElement="Flow_TwoOrThree">
<di:waypoint x="965" y="117" />
<di:waypoint x="1070" y="117" />
<bpmndi:BPMNLabel>
<dc:Bounds x="987" y="86" width="65" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_10rb7gb_di" bpmnElement="Flow_10rb7gb">
<di:waypoint x="860" y="117" />
<di:waypoint x="915" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0jqdolk_di" bpmnElement="Flow_0jqdolk">
<di:waypoint x="1380" y="117" />
<di:waypoint x="1492" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1p563xr_di" bpmnElement="Flow_1p563xr">
<di:waypoint x="1170" y="117" />
<di:waypoint x="1280" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1bn0jp7_di" bpmnElement="Flow_1bn0jp7">
<di:waypoint x="670" y="117" />
<di:waypoint x="760" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1cwibmt_di" bpmnElement="SequenceFlow_1cwibmt">
<di:waypoint x="520" y="117" />
<di:waypoint x="570" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1uzcl1f_di" bpmnElement="SequenceFlow_1uzcl1f">
<di:waypoint x="340" y="117" />
<di:waypoint x="420" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1dhb8f4_di" bpmnElement="SequenceFlow_1dhb8f4">
<di:waypoint x="188" y="117" />
<di:waypoint x="240" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1h89sl4_di" bpmnElement="EndEvent_1h89sl4">
<dc:Bounds x="1492" y="99" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="414" y="202" width="74" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1fn00ox_di" bpmnElement="ScriptTask_LoadIRBDetails">
<dc:Bounds x="240" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_1cszgkx_di" bpmnElement="Task_SupplementIDE">
<dc:Bounds x="420" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0a2dfa8_di" bpmnElement="IND_n1_info">
<dc:Bounds x="760" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1smni98_di" bpmnElement="IND_n2_info">
<dc:Bounds x="1070" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1378hd8_di" bpmnElement="IND_n3_info">
<dc:Bounds x="1280" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0wfey2b_di" bpmnElement="Activity_0yf2ypo">
<dc:Bounds x="570" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0ckycp9_di" bpmnElement="Gateway_0ckycp9" isMarkerVisible="true">
<dc:Bounds x="915" y="92" width="50" height="50" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_00j2iu5" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_00j2iu5" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_1gmf4la" isExecutable="true">
<bpmn:documentation />
<bpmn:startEvent id="StartEvent_1">
@ -8,7 +8,7 @@
<bpmn:scriptTask id="ScriptTask_02924vs" name="Load IRB Details">
<bpmn:incoming>SequenceFlow_1fmyo77</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_18nr0gf</bpmn:outgoing>
<bpmn:script>StudyInfo details</bpmn:script>
<bpmn:script>#! StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1fmyo77" sourceRef="StartEvent_1" targetRef="ScriptTask_02924vs" />
<bpmn:sequenceFlow id="SequenceFlow_18nr0gf" sourceRef="ScriptTask_02924vs" targetRef="Activity_FromIRB-API" />

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_06pyjz2" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_06pyjz2" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_01143nb" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0kcrx5l</bpmn:outgoing>
@ -7,30 +7,41 @@
<bpmn:scriptTask id="ScriptTask_LoadPersonnel" name="Load IRB Personnel">
<bpmn:incoming>Flow_0kcrx5l</bpmn:incoming>
<bpmn:outgoing>Flow_1dcsioh</bpmn:outgoing>
<bpmn:script>StudyInfo investigators</bpmn:script>
<bpmn:script>#! StudyInfo investigators</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_1qor16n">
<bpmn:documentation>## The following information was gathered:
{% for type, investigator in StudyInfo.investigators.items() %}
### {{investigator.label}}: {{investigator.display_name}}
* Edit Acess? {{investigator.edit_access}}
* Send Emails? {{investigator.emails}}
{% if investigator.label == "Primary Investigator" %}
* Experience: {{investigator.experience}}
{% endif %}
{% endfor %}</bpmn:documentation>
<bpmn:incoming>Flow_1mplloa</bpmn:incoming>
</bpmn:endEvent>
<bpmn:userTask id="Activity_EditOtherPersonnel" name="Supplement Personnel" camunda:formKey="Access &#38; Notifications">
<bpmn:userTask id="Activity_EditOtherPersonnel" name="Update Personnel" camunda:formKey="Access &#38; Notifications">
<bpmn:documentation>### Please provide supplemental information for:
#### Investigator : {{investigator.display_name}}
##### Role: {{investigator.type_full}}
#### {{investigator.display_name}}
##### Title: {{investigator.title}}
##### Department: {{investigator.department}}
##### Affiliation: {{investigator.affiliation}}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="EditAccess" label="Should have Study Team editing access in the system?" type="boolean" defaultValue="false" />
<camunda:formField id="AutomatedEmailNotification" label="Should receive automated email notifications?" type="boolean" defaultValue="false" />
<camunda:formField id="PI_Experience" label="Investigator&#39;s Experience" type="textarea">
<camunda:formField id="investigator.edit_access" label="Should have Study Team editing access in the system?" type="boolean" defaultValue="false" />
<camunda:formField id="investigator.emails" label="Should receive automated email notifications?" type="boolean" defaultValue="false" />
<camunda:formField id="investigator.experience" label="Investigator&#39;s Experience" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
<camunda:property id="hide_expression" value="model.investigator.type_full !== &#34;Primary Investigator&#34;" />
<camunda:property id="hide_expression" value="model.investigator.label !== &#34;Primary Investigator&#34;" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
<camunda:properties>
<camunda:property name="display_name" value="investigator.label" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1dcsioh</bpmn:incoming>
<bpmn:outgoing>Flow_1mplloa</bpmn:outgoing>
@ -43,28 +54,28 @@
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_01143nb">
<bpmndi:BPMNEdge id="Flow_1dcsioh_di" bpmnElement="Flow_1dcsioh">
<di:waypoint x="370" y="120" />
<di:waypoint x="450" y="120" />
<di:waypoint x="360" y="120" />
<di:waypoint x="420" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1mplloa_di" bpmnElement="Flow_1mplloa">
<di:waypoint x="550" y="120" />
<di:waypoint x="642" y="120" />
<di:waypoint x="520" y="120" />
<di:waypoint x="602" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0kcrx5l_di" bpmnElement="Flow_0kcrx5l">
<di:waypoint x="188" y="120" />
<di:waypoint x="270" y="120" />
<di:waypoint x="260" y="120" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_0h49cmf_di" bpmnElement="ScriptTask_LoadPersonnel">
<dc:Bounds x="270" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1qor16n_di" bpmnElement="EndEvent_1qor16n">
<dc:Bounds x="642" y="102" width="36" height="36" />
<dc:Bounds x="260" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0d622qi_di" bpmnElement="Activity_EditOtherPersonnel">
<dc:Bounds x="450" y="80" width="100" height="80" />
<dc:Bounds x="420" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1qor16n_di" bpmnElement="EndEvent_1qor16n">
<dc:Bounds x="602" y="102" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1oogn9j" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1oogn9j" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0ssahs9" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_05ja25w</bpmn:outgoing>
@ -598,7 +598,7 @@ Use the EHS [Lab Safety Plan During COVID 19 template](https://www.google.com/ur
This step is internal to the system and do not require and user interaction</bpmn:documentation>
<bpmn:incoming>Flow_11uqavk</bpmn:incoming>
<bpmn:outgoing>Flow_0aqgwvu</bpmn:outgoing>
<bpmn:script>CompleteTemplate ResearchRampUpPlan.docx RESEARCH_RAMPUP</bpmn:script>
<bpmn:script>#! CompleteTemplate ResearchRampUpPlan.docx RESEARCH_RAMPUP</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0aqgwvu" sourceRef="Activity_GenerateRRP" targetRef="Activity_AcknowledgePlanReview" />
<bpmn:sequenceFlow id="Flow_0j4rs82" sourceRef="Activity_SubmitPlan" targetRef="Activity_0absozl" />
@ -755,7 +755,7 @@ Notify the Area Monitor for
This step is internal to the system and do not require and user interaction</bpmn:documentation>
<bpmn:incoming>Flow_0j4rs82</bpmn:incoming>
<bpmn:outgoing>Flow_07ge8uf</bpmn:outgoing>
<bpmn:script>RequestApproval ApprvlApprvr1 ApprvlApprvr2</bpmn:script>
<bpmn:script>#!RequestApproval ApprvlApprvr1 ApprvlApprvr2</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_1u58hox" name="Update Request">
<bpmn:documentation>#### Script Task
@ -764,7 +764,7 @@ This step is internal to the system and do not require and user interaction</bpm
This step is internal to the system and do not require and user interaction</bpmn:documentation>
<bpmn:incoming>Flow_16y8glw</bpmn:incoming>
<bpmn:outgoing>Flow_0uc4o6c</bpmn:outgoing>
<bpmn:script>UpdateStudy title:PIComputingID.label pi:PIComputingID.value</bpmn:script>
<bpmn:script>#! UpdateStudy title:PIComputingID.label pi:PIComputingID.value</bpmn:script>
</bpmn:scriptTask>
<bpmn:userTask id="PersonnelSchedule" name="Upload Weekly Personnel Schedule(s)" camunda:formKey="Personnel Weekly Schedule">
<bpmn:documentation>#### Weekly Personnel Schedule(s)

View File

@ -11,7 +11,7 @@
<bpmn:scriptTask id="Task_Load_Requirements" name="Load Documents From PB">
<bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>StudyInfo documents</bpmn:script>
<bpmn:script>#! StudyInfo documents</bpmn:script>
</bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<bpmn:incoming>Flow_1m8285h</bpmn:incoming>
@ -62,7 +62,7 @@
<bpmn:scriptTask id="Activity_0f295la" name="Load Details from PB">
<bpmn:incoming>Flow_0pwtiqm</bpmn:incoming>
<bpmn:outgoing>Flow_0eq6px2</bpmn:outgoing>
<bpmn:script>StudyInfo details</bpmn:script>
<bpmn:script>#! StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_0ahlc3u" name="IDE Supplement" camunda:decisionRef="decision_ide_menu_check">
<bpmn:incoming>Flow_14ce1d7</bpmn:incoming>
@ -91,7 +91,7 @@
<bpmn:scriptTask id="Activity_0g3qa1c" name="Load Personnel from PB">
<bpmn:incoming>Flow_1qyrmzn</bpmn:incoming>
<bpmn:outgoing>Flow_0vo6ul1</bpmn:outgoing>
<bpmn:script>StudyInfo investigators</bpmn:script>
<bpmn:script>#! StudyInfo investigators</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1ybicki" sourceRef="Activity_13ep6ar" targetRef="Event_135x8jg" />
<bpmn:businessRuleTask id="Activity_13ep6ar" name="Personnel" camunda:decisionRef="personnel">

View File

@ -1 +1 @@
Your Research Ramp-up Plan has been denied by {{ approver_1 }}. Please return to the Research Ramp-up Plan application and review the comments from {{ approver_1 }} on the home page. Next, open the application and locate the first step where changes are needed. Continue to complete additional steps saving your work along the way. Review your revised Research Ramp-up Plan and res-submit for approval.
Your Research Ramp-up Plan has been denied by {{ approver }}. Please return to the Research Ramp-up Plan application and review the comments from {{ approver }} on the home page. Next, open the application and locate the first step where changes are needed. Continue to complete additional steps saving your work along the way. Review your revised Research Ramp-up Plan and res-submit for approval.

View File

@ -23,8 +23,10 @@ if [ "$RESET_DB_RRT" = "true" ]; then
pipenv run flask load-example-rrt-data
fi
# THIS MUST BE THE LAST COMMAND!
if [ "$APPLICATION_ROOT" = "/" ]; then
pipenv run gunicorn --bind 0.0.0.0:$PORT0 wsgi:app
else
pipenv run gunicorn -e SCRIPT_NAME="$APPLICATION_ROOT" --bind 0.0.0.0:$PORT0 wsgi:app
fi

View File

@ -93,8 +93,8 @@ class ExampleDataLoader:
description="Supplemental information for the IDE number entered in Protocol Builder",
category_id=0,
display_order=3)
self.create_spec(id="ind_supplement",
name="ind_supplement",
self.create_spec(id="ind_update",
name="ind_update",
display_name="IND Supplement Info",
description="Supplement information for the Investigational New Drug(s) specified in Protocol Builder",
category_id=0,

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: 1fdd1bdb600e
Revises: 17597692d0b0
Create Date: 2020-06-17 16:44:16.427988
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1fdd1bdb600e'
down_revision = '17597692d0b0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_event', sa.Column('task_data', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('task_event', 'task_data')
# ### end Alembic commands ###

View File

@ -0,0 +1,38 @@
"""empty message
Revision ID: 5acd138e969c
Revises: de30304ff5e6
Create Date: 2020-06-24 21:36:15.128632
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5acd138e969c'
down_revision = 'de30304ff5e6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('email',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('subject', sa.String(), nullable=True),
sa.Column('sender', sa.String(), nullable=True),
sa.Column('recipients', sa.String(), nullable=True),
sa.Column('content', sa.String(), nullable=True),
sa.Column('content_html', sa.String(), nullable=True),
sa.Column('study_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('email')
# ### end Alembic commands ###

View File

@ -0,0 +1,30 @@
"""empty message
Revision ID: de30304ff5e6
Revises: 1fdd1bdb600e
Create Date: 2020-06-18 16:19:11.133665
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'de30304ff5e6'
down_revision = '1fdd1bdb600e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_event', sa.Column('form_data', sa.JSON(), nullable=True))
op.drop_column('task_event', 'task_data')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_event', sa.Column('task_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
op.drop_column('task_event', 'form_data')
# ### end Alembic commands ###

View File

@ -0,0 +1,38 @@
"""empty message
Revision ID: ffef4661a37d
Revises: 5acd138e969c
Create Date: 2020-07-14 19:52:05.270939
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ffef4661a37d'
down_revision = '5acd138e969c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_event', sa.Column('task_lane', sa.String(), nullable=True))
op.drop_constraint('task_event_user_uid_fkey', 'task_event', type_='foreignkey')
op.execute("update task_event set action = 'COMPLETE' where action='Complete'")
op.execute("update task_event set action = 'TOKEN_RESET' where action='Backwards Move'")
op.execute("update task_event set action = 'HARD_RESET' where action='Restart (Hard)'")
op.execute("update task_event set action = 'SOFT_RESET' where action='Restart (Soft)'")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key('task_event_user_uid_fkey', 'task_event', 'user', ['user_uid'], ['uid'])
op.drop_column('task_event', 'task_lane')
op.execute("update task_event set action = 'Complete' where action='COMPLETE'")
op.execute("update task_event set action = 'Backwards Move' where action='TOKEN_RESET'")
op.execute("update task_event set action = 'Restart (Hard)' where action='HARD_RESET'")
op.execute("update task_event set action = 'Restart (Soft)' where action='SOFT_RESET'")
# ### end Alembic commands ###

View File

@ -1,9 +1,8 @@
version: "3.7"
services:
db:
image: postgres
image: sartography/cr-connect-db
volumes:
- ./pg-init-scripts/initdb.sh:/docker-entrypoint-initdb.d/initdb.sh
- $HOME/docker/volumes/postgres:/var/lib/postgresql/data
ports:
- 5432:5432

View File

@ -217,27 +217,6 @@ class TestApprovals(BaseTest):
total_counts = sum(counts[status] for status in statuses)
self.assertEqual(total_counts, len(approvals), 'Total approval counts for user should match number of approvals for user')
def _create_study_workflow_approvals(self, user_uid, title, primary_investigator_id, approver_uids, statuses,
workflow_spec_name="random_fact"):
study = self.create_study(uid=user_uid, title=title, primary_investigator_id=primary_investigator_id)
workflow = self.create_workflow(workflow_name=workflow_spec_name, study=study)
approvals = []
for i in range(len(approver_uids)):
approvals.append(self.create_approval(
study=study,
workflow=workflow,
approver_uid=approver_uids[i],
status=statuses[i],
version=1
))
return {
'study': study,
'workflow': workflow,
'approvals': approvals,
}
def _add_lots_of_random_approvals(self, n=100, workflow_spec_name="random_fact"):
num_studies_before = db.session.query(StudyModel).count()
statuses = [name for name, value in ApprovalStatus.__members__.items()]

View File

@ -1,7 +1,7 @@
from tests.base_test import BaseTest
from crc import db
from crc.models.approval import ApprovalModel
from crc.services.approval_service import ApprovalService
from crc.services.approval_service import ApprovalService, ApprovalStatus
from crc.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor
@ -57,6 +57,60 @@ class TestApprovalsService(BaseTest):
self.assertEqual(1, models[0].version)
self.assertEqual(2, models[1].version)
def test_get_health_attesting_records(self):
self.load_example_data()
self.create_reference_document()
workflow = self.create_workflow('empty_workflow')
FileService.add_workflow_file(workflow_id=workflow.id,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code="AD_CoCAppr")
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
records = ApprovalService.get_health_attesting_records()
self.assertEqual(len(records), 1)
def test_get_not_really_csv_content(self):
self.load_example_data()
self.create_reference_document()
workflow = self.create_workflow('empty_workflow')
FileService.add_workflow_file(workflow_id=workflow.id,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code="AD_CoCAppr")
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
records = ApprovalService.get_not_really_csv_content()
self.assertEqual(len(records), 2)
def test_new_approval_cancels_all_previous_approvals(self):
self.create_reference_document()
workflow = self.create_workflow("empty_workflow")
FileService.add_workflow_file(workflow_id=workflow.id,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr" )
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="lb3dp")
current_count = ApprovalModel.query.count()
self.assertTrue(current_count, 2)
FileService.add_workflow_file(workflow_id=workflow.id,
name="borderline.png", content_type="text",
binary_data=b'906090', irb_doc_code="AD_CoCAppr" )
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
current_count = ApprovalModel.query.count()
canceled_count = ApprovalModel.query.filter(ApprovalModel.status == ApprovalStatus.CANCELED.value)
self.assertTrue(current_count, 2)
self.assertTrue(current_count, 3)
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="lb3dp")
current_count = ApprovalModel.query.count()
self.assertTrue(current_count, 4)
def test_new_approval_sends_proper_emails(self):
self.assertEqual(1, 1)

View File

@ -16,7 +16,7 @@ from crc.models.api_models import WorkflowApiSchema, MultiInstanceType
from crc.models.approval import ApprovalModel, ApprovalStatus
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel
@ -230,7 +230,7 @@ class BaseTest(unittest.TestCase):
db.session.commit()
return user
def create_study(self, uid="dhf8r", title="Beer conception in the bipedal software engineer", primary_investigator_id="lb3dp"):
def create_study(self, uid="dhf8r", title="Beer consumption in the bipedal software engineer", primary_investigator_id="lb3dp"):
study = session.query(StudyModel).filter_by(user_uid=uid).filter_by(title=title).first()
if study is None:
user = self.create_user(uid=uid)
@ -240,13 +240,36 @@ class BaseTest(unittest.TestCase):
db.session.commit()
return study
def create_workflow(self, workflow_name, study=None, category_id=None):
def _create_study_workflow_approvals(self, user_uid, title, primary_investigator_id, approver_uids, statuses,
workflow_spec_name="random_fact"):
study = self.create_study(uid=user_uid, title=title, primary_investigator_id=primary_investigator_id)
workflow = self.create_workflow(workflow_name=workflow_spec_name, study=study)
approvals = []
for i in range(len(approver_uids)):
approvals.append(self.create_approval(
study=study,
workflow=workflow,
approver_uid=approver_uids[i],
status=statuses[i],
version=1
))
full_study = {
'study': study,
'workflow': workflow,
'approvals': approvals,
}
return full_study
def create_workflow(self, workflow_name, study=None, category_id=None, as_user="dhf8r"):
db.session.flush()
spec = db.session.query(WorkflowSpecModel).filter(WorkflowSpecModel.name == workflow_name).first()
if spec is None:
spec = self.load_test_spec(workflow_name, category_id=category_id)
if study is None:
study = self.create_study()
study = self.create_study(uid=as_user)
workflow_model = StudyService._create_workflow_model(study, spec)
return workflow_model
@ -290,7 +313,8 @@ class BaseTest(unittest.TestCase):
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
return workflow_api
def complete_form(self, workflow_in, task_in, dict_data, error_code=None, user_uid="dhf8r"):
def complete_form(self, workflow_in, task_in, dict_data, error_code=None, terminate_loop=None, user_uid="dhf8r"):
prev_completed_task_count = workflow_in.completed_tasks
if isinstance(task_in, dict):
task_id = task_in["id"]
@ -299,11 +323,16 @@ class BaseTest(unittest.TestCase):
user = session.query(UserModel).filter_by(uid=user_uid).first()
self.assertIsNotNone(user)
rv = self.app.put('/v1.0/workflow/%i/task/%s/data' % (workflow_in.id, task_id),
headers=self.logged_in_headers(user=user),
content_type="application/json",
data=json.dumps(dict_data))
if terminate_loop:
rv = self.app.put('/v1.0/workflow/%i/task/%s/data?terminate_loop=true' % (workflow_in.id, task_id),
headers=self.logged_in_headers(user=user),
content_type="application/json",
data=json.dumps(dict_data))
else:
rv = self.app.put('/v1.0/workflow/%i/task/%s/data' % (workflow_in.id, task_id),
headers=self.logged_in_headers(user=user),
content_type="application/json",
data=json.dumps(dict_data))
if error_code:
self.assert_failure(rv, error_code=error_code)
return
@ -311,17 +340,20 @@ class BaseTest(unittest.TestCase):
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
# Assure stats are updated on the model
# Assure task events are updated on the model
workflow = WorkflowApiSchema().load(json_data)
# The total number of tasks may change over time, as users move through gateways
# branches may be pruned. As we hit parallel Multi-Instance new tasks may be created...
self.assertIsNotNone(workflow.total_tasks)
self.assertEqual(prev_completed_task_count + 1, workflow.completed_tasks)
# presumably, we also need to deal with sequential items here too . .
if not task_in.multi_instance_type == 'looping':
self.assertEqual(prev_completed_task_count + 1, workflow.completed_tasks)
# Assure a record exists in the Task Events
task_events = session.query(TaskEventModel) \
.filter_by(workflow_id=workflow.id) \
.filter_by(task_id=task_id) \
.filter_by(action=WorkflowService.TASK_ACTION_COMPLETE) \
.order_by(TaskEventModel.date.desc()).all()
self.assertGreater(len(task_events), 0)
event = task_events[0]
@ -335,7 +367,8 @@ class BaseTest(unittest.TestCase):
self.assertEqual(task_in.name, event.task_name)
self.assertEqual(task_in.title, event.task_title)
self.assertEqual(task_in.type, event.task_type)
self.assertEqual("COMPLETED", event.task_state)
if not task_in.multi_instance_type == 'looping':
self.assertEqual("COMPLETED", event.task_state)
# Not sure what voodoo is happening inside of marshmallow to get me in this state.
if isinstance(task_in.multi_instance_type, MultiInstanceType):
@ -344,7 +377,10 @@ class BaseTest(unittest.TestCase):
self.assertEqual(task_in.multi_instance_type, event.mi_type)
self.assertEqual(task_in.multi_instance_count, event.mi_count)
self.assertEqual(task_in.multi_instance_index, event.mi_index)
if task_in.multi_instance_type == 'looping' and not terminate_loop:
self.assertEqual(task_in.multi_instance_index+1, event.mi_index)
else:
self.assertEqual(task_in.multi_instance_index, event.mi_index)
self.assertEqual(task_in.process_name, event.process_name)
self.assertIsNotNone(event.date)

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_1hao5sb" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<decision id="bad_dmn" name="Decision 1">
<extensionElements>
<biodi:bounds x="150" y="150" width="180" height="80" />
</extensionElements>
<decisionTable id="decisionTable_1">
<input id="input_1" label="num_presents">
<inputExpression id="inputExpression_1" typeRef="long">
<text>1</text>
</inputExpression>
</input>
<output id="output_1" label="My Message" name="message" typeRef="integer" />
<rule id="DecisionRule_0gl355z">
<description></description>
<inputEntry id="UnaryTests_06x22gk">
<text>0</text>
</inputEntry>
<outputEntry id="LiteralExpression_0yuxzxi">
<text>0</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1s6l5b6">
<description>'one' can't be evaluated, it must be quoted</description>
<inputEntry id="UnaryTests_1oyo6k0">
<text>1</text>
</inputEntry>
<outputEntry id="LiteralExpression_09t5r62">
<text>one</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1dvd34d">
<inputEntry id="UnaryTests_1k557bj">
<text>2</text>
</inputEntry>
<outputEntry id="LiteralExpression_1n1eo23">
<text>2</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0tqqjg9">
<inputEntry id="UnaryTests_0dnd50d">
<text>&gt; 2</text>
</inputEntry>
<outputEntry id="LiteralExpression_0fk5uhh">
<text>3</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -0,0 +1,56 @@
<?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_1elv5t1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_15vbyda" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1ma1wxb</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1ma1wxb" sourceRef="StartEvent_1" targetRef="Task_0sgafty" />
<bpmn:businessRuleTask id="Task_0sgafty" name="A bad DMN" camunda:decisionRef="bad_dmn">
<bpmn:incoming>SequenceFlow_1ma1wxb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0grui6f</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:endEvent id="EndEvent_0tsqkyu">
<bpmn:documentation># Great Work!
Based on the information you provided (Ginger left {{num_presents}}, we recommend the following statement be provided to Ginger:
## {{message}}
We hope you both have an excellent day!</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0grui6f</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0grui6f" sourceRef="Task_0sgafty" targetRef="EndEvent_0tsqkyu" />
<bpmn:textAnnotation id="TextAnnotation_0oajoz7">
<bpmn:text>This DMN isn't provided enough information to execute</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1raak4y" sourceRef="Task_0sgafty" targetRef="TextAnnotation_0oajoz7" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_15vbyda">
<bpmndi:BPMNShape id="TextAnnotation_0oajoz7_di" bpmnElement="TextAnnotation_0oajoz7">
<dc:Bounds x="400" y="80" width="100" height="82" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0grui6f_di" bpmnElement="SequenceFlow_0grui6f">
<di:waypoint x="370" y="237" />
<di:waypoint x="432" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1ma1wxb_di" bpmnElement="SequenceFlow_1ma1wxb">
<di:waypoint x="215" y="237" />
<di:waypoint x="270" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="219" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_10c5wgr_di" bpmnElement="Task_0sgafty">
<dc:Bounds x="270" y="197" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0tsqkyu_di" bpmnElement="EndEvent_0tsqkyu">
<dc:Bounds x="432" y="219" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1raak4y_di" bpmnElement="Association_1raak4y">
<di:waypoint x="364" y="198" />
<di:waypoint x="404" y="162" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96a17d9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<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_96a17d9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_93a29b3" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0637d8i</bpmn:outgoing>
@ -27,7 +27,7 @@
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1i7hk1a</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_11c35oq</bpmn:outgoing>
<bpmn:script>CompleteTemplate Letter.docx AD_CoCApp</bpmn:script>
<bpmn:script>#! CompleteTemplate Letter.docx AD_CoCApp</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0evb22x">
<bpmn:incoming>SequenceFlow_11c35oq</bpmn:incoming>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0y2dq4f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0tad5ma" name="Set Recipients" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1synsig</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:endEvent id="Event_0izrcj4">
<bpmn:incoming>Flow_1xlrgne</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="Activity_0s5v97n" name="Email Recipients">
<bpmn:documentation># Dear Approver
## you have been requested for approval
---
New request submitted by {{ PIComputingID }}
Email content to be delivered to {{ ApprvlApprvr1 }}
---</bpmn:documentation>
<bpmn:incoming>Flow_08n2npe</bpmn:incoming>
<bpmn:outgoing>Flow_1xlrgne</bpmn:outgoing>
<bpmn:script>#! Email "Camunda Email Subject" ApprvlApprvr1 PIComputingID</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1synsig" sourceRef="StartEvent_1" targetRef="Activity_1l9vih3" />
<bpmn:sequenceFlow id="Flow_1xlrgne" sourceRef="Activity_0s5v97n" targetRef="Event_0izrcj4" />
<bpmn:sequenceFlow id="Flow_08n2npe" sourceRef="Activity_1l9vih3" targetRef="Activity_0s5v97n" />
<bpmn:userTask id="Activity_1l9vih3" name="Set Recipients">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="ApprvlApprvr1" label="Approver" type="string" />
<camunda:formField id="PIComputingID" label="Primary Investigator" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1synsig</bpmn:incoming>
<bpmn:outgoing>Flow_08n2npe</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0tad5ma">
<bpmndi:BPMNEdge id="Flow_08n2npe_di" bpmnElement="Flow_08n2npe">
<di:waypoint x="370" y="117" />
<di:waypoint x="450" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1xlrgne_di" bpmnElement="Flow_1xlrgne">
<di:waypoint x="550" y="117" />
<di:waypoint x="662" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1synsig_di" bpmnElement="Flow_1synsig">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0izrcj4_di" bpmnElement="Event_0izrcj4">
<dc:Bounds x="662" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_04imfm6_di" bpmnElement="Activity_0s5v97n">
<dc:Bounds x="450" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0xugr62_di" bpmnElement="Activity_1l9vih3">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,100 @@
<?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_1v1rp1q" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.0.0">
<bpmn:process id="Process_1vu5nxl" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0lvudp8</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0lvudp8" sourceRef="StartEvent_1" targetRef="Activity_0oa736e" />
<bpmn:endEvent id="EndEvent_0q4qzl9">
<bpmn:incoming>SequenceFlow_02vev7n</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_02vev7n" sourceRef="Task_14svgcu" targetRef="EndEvent_0q4qzl9" />
<bpmn:userTask id="Task_14svgcu" name="Enum Lookup Form" camunda:formKey="EnumForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="guest_of_honor" label="Who is the guest of honor?" type="enum">
<camunda:properties>
<camunda:property id="data.name" value="invitees" />
<camunda:property id="data.value.column" value="secret_id" />
<camunda:property id="data.label.column" value="display_name" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1yet4a9</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_02vev7n</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1yet4a9" sourceRef="Activity_0oa736e" targetRef="Task_14svgcu" />
<bpmn:userTask id="Activity_0oa736e" name="Who do you want to invite to your tea party?" camunda:formKey="task_1_form">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="first_name" label="What is their first name?" type="string">
<camunda:properties>
<camunda:property id="repeat" value="invitees" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="last_name" label="What is their last name?" type="string">
<camunda:properties>
<camunda:property id="repeat" value="invitees" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="age" label="How old are they?" type="long">
<camunda:properties>
<camunda:property id="repeat" value="invitees" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="likes_pie" label="Do they like pie?" type="boolean">
<camunda:properties>
<camunda:property id="repeat" value="invitees" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="num_lumps" label="How many lumps of sugar in their tea?" type="long">
<camunda:properties>
<camunda:property id="repeat" value="invitees" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="secret_id" label="What is their secret identity?" type="string">
<camunda:properties>
<camunda:property id="repeat" value="invitees" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="display_name" label="What&#39;s their nickname?" type="string">
<camunda:properties>
<camunda:property id="repeat" value="invitees" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0lvudp8</bpmn:incoming>
<bpmn:outgoing>Flow_1yet4a9</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1vu5nxl">
<bpmndi:BPMNEdge id="SequenceFlow_02vev7n_di" bpmnElement="SequenceFlow_02vev7n">
<di:waypoint x="500" y="117" />
<di:waypoint x="542" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0lvudp8_di" bpmnElement="SequenceFlow_0lvudp8">
<di:waypoint x="215" y="117" />
<di:waypoint x="250" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1yet4a9_di" bpmnElement="Flow_1yet4a9">
<di:waypoint x="350" y="117" />
<di:waypoint x="400" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0k8rwp9_di" bpmnElement="Activity_0oa736e">
<dc:Bounds x="250" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_18ly1yq_di" bpmnElement="Task_14svgcu">
<dc:Bounds x="400" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0q4qzl9_di" bpmnElement="EndEvent_0q4qzl9">
<dc:Bounds x="542" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,177 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0ybr9ph" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:collaboration id="Collaboration_0xjb3la">
<bpmn:participant id="Participant_0ozb2sp" processRef="Process_1aebbrh" />
</bpmn:collaboration>
<bpmn:process id="Process_1aebbrh" isExecutable="true">
<bpmn:laneSet id="LaneSet_0ilprw6">
<bpmn:lane id="Lane_1s1s7a1">
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_1hljoeq</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Event_0lscajc</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_19ccxoj</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="Lane_1m47545" name="supervisor">
<bpmn:flowNodeRef>Gateway_1fkgc4u</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_14eor1x</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0a7090c</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Activity_1hljoeq" name="Request Approval" camunda:formKey="form">
<bpmn:documentation># Answer me these questions 3, ere the other side you see!</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="favorite_color" label="What is your favorite color?" type="string" defaultValue="Yellow" />
<camunda:formField id="quest" label="What is your quest?" type="string" defaultValue="To seek the holly Grail!" />
<camunda:formField id="swallow_speed" label="What is the air speed velocity of an unladen swallow?" defaultValue="About 24 miles per hour" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0a7090c</bpmn:incoming>
<bpmn:incoming>Flow_070gq5r</bpmn:incoming>
<bpmn:outgoing>Flow_1hcpt7c</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_1fkgc4u">
<bpmn:incoming>Flow_1gp4zfd</bpmn:incoming>
<bpmn:outgoing>Flow_0vnghsi</bpmn:outgoing>
<bpmn:outgoing>Flow_1g38q6b</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="Event_0lscajc">
<bpmn:documentation># Your responses were approved!
Gosh! you must really know a lot about colors and swallows and stuff!
Your supervisor provided the following feedback:
{{feedback}}
You are all done! WARNING: If you go back and reanswer the questions it will create a new approval request.
</bpmn:documentation>
<bpmn:incoming>Flow_1g38q6b</bpmn:incoming>
</bpmn:endEvent>
<bpmn:manualTask id="Activity_19ccxoj" name="Review Feedback">
<bpmn:documentation># Your Request was rejected
Perhaps you don't know the right answer to one of the questions.
Your Supervisor provided the following feedback:
{{feedback}}
Please press save to re-try the questions, and submit your responses again.
</bpmn:documentation>
<bpmn:incoming>Flow_0vnghsi</bpmn:incoming>
<bpmn:outgoing>Flow_070gq5r</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:userTask id="Activity_14eor1x" name="Approve Responses" camunda:formKey="form2">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="approval" label="I approve of this information" type="boolean" defaultValue="false" />
<camunda:formField id="feedback" label="Feedback" type="string" defaultValue="Please provide any feedback you have here." />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1hcpt7c</bpmn:incoming>
<bpmn:outgoing>Flow_1gp4zfd</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0a7090c" sourceRef="StartEvent_1" targetRef="Activity_1hljoeq" />
<bpmn:sequenceFlow id="Flow_1gp4zfd" sourceRef="Activity_14eor1x" targetRef="Gateway_1fkgc4u" />
<bpmn:sequenceFlow id="Flow_0vnghsi" name="rejected" sourceRef="Gateway_1fkgc4u" targetRef="Activity_19ccxoj">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1g38q6b" name="approved" sourceRef="Gateway_1fkgc4u" targetRef="Event_0lscajc">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1hcpt7c" sourceRef="Activity_1hljoeq" targetRef="Activity_14eor1x" />
<bpmn:sequenceFlow id="Flow_070gq5r" sourceRef="Activity_19ccxoj" targetRef="Activity_1hljoeq" />
<bpmn:textAnnotation id="TextAnnotation_1ys83yq">
<bpmn:text>Removed a field that would set the supervisor, making this not validate.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1kcb9ou" sourceRef="Activity_1hljoeq" targetRef="TextAnnotation_1ys83yq" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0xjb3la">
<bpmndi:BPMNShape id="Participant_0ozb2sp_di" bpmnElement="Participant_0ozb2sp" isHorizontal="true">
<dc:Bounds x="190" y="80" width="550" height="370" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1s1s7a1_di" bpmnElement="Lane_1s1s7a1" isHorizontal="true">
<dc:Bounds x="220" y="80" width="520" height="245" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1m47545_di" bpmnElement="Lane_1m47545" isHorizontal="true">
<dc:Bounds x="220" y="325" width="520" height="125" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1ys83yq_di" bpmnElement="TextAnnotation_1ys83yq">
<dc:Bounds x="250" y="100" width="130.6238034460753" height="68.28334396936822" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0a7090c_di" bpmnElement="Flow_0a7090c">
<di:waypoint x="276" y="260" />
<di:waypoint x="330" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gp4zfd_di" bpmnElement="Flow_1gp4zfd">
<di:waypoint x="430" y="390" />
<di:waypoint x="485" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vnghsi_di" bpmnElement="Flow_0vnghsi">
<di:waypoint x="510" y="365" />
<di:waypoint x="510" y="300" />
<bpmndi:BPMNLabel>
<dc:Bounds x="520" y="334" width="40" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1g38q6b_di" bpmnElement="Flow_1g38q6b">
<di:waypoint x="535" y="390" />
<di:waypoint x="680" y="390" />
<di:waypoint x="680" y="278" />
<bpmndi:BPMNLabel>
<dc:Bounds x="585" y="372" width="46" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hcpt7c_di" bpmnElement="Flow_1hcpt7c">
<di:waypoint x="380" y="300" />
<di:waypoint x="380" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_070gq5r_di" bpmnElement="Flow_070gq5r">
<di:waypoint x="510" y="220" />
<di:waypoint x="510" y="160" />
<di:waypoint x="380" y="160" />
<di:waypoint x="380" y="220" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="240" y="242" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0xcxw40_di" bpmnElement="Activity_1hljoeq">
<dc:Bounds x="330" y="220" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1fkgc4u_di" bpmnElement="Gateway_1fkgc4u" isMarkerVisible="true">
<dc:Bounds x="485" y="365" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0lscajc_di" bpmnElement="Event_0lscajc">
<dc:Bounds x="662" y="242" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1jfdeta_di" bpmnElement="Activity_19ccxoj">
<dc:Bounds x="460" y="220" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zc7cgy_di" bpmnElement="Activity_14eor1x">
<dc:Bounds x="330" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1kcb9ou_di" bpmnElement="Association_1kcb9ou">
<di:waypoint x="359" y="220" />
<di:waypoint x="333" y="168" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1j7idla" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1j7idla" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_18biih5" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1pnq3kg</bpmn:outgoing>
@ -11,7 +11,7 @@
<bpmn:scriptTask id="Invalid_Script_Task" name="An Invalid Script Reference">
<bpmn:incoming>SequenceFlow_1pnq3kg</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_12pf6um</bpmn:outgoing>
<bpmn:script>NoSuchScript withArg1</bpmn:script>
<bpmn:script>#! NoSuchScript withArg1</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_12pf6um" sourceRef="Invalid_Script_Task" targetRef="EndEvent_063bpg6" />
</bpmn:process>

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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1j7idla" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_18biih5" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1pnq3kg</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1pnq3kg" sourceRef="StartEvent_1" targetRef="Invalid_Script_Task" />
<bpmn:endEvent id="EndEvent_063bpg6">
<bpmn:incoming>SequenceFlow_12pf6um</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="Invalid_Script_Task" name="An Invalid Script Reference">
<bpmn:incoming>SequenceFlow_1pnq3kg</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_12pf6um</bpmn:outgoing>
<bpmn:script>a really bad error that should fail</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_12pf6um" sourceRef="Invalid_Script_Task" targetRef="EndEvent_063bpg6" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_18biih5">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1pnq3kg_di" bpmnElement="SequenceFlow_1pnq3kg">
<di:waypoint x="215" y="117" />
<di:waypoint x="290" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_063bpg6_di" bpmnElement="EndEvent_063bpg6">
<dc:Bounds x="442" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1imeym0_di" bpmnElement="Invalid_Script_Task">
<dc:Bounds x="290" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_12pf6um_di" bpmnElement="SequenceFlow_12pf6um">
<di:waypoint x="390" y="117" />
<di:waypoint x="442" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,155 +1,124 @@
{
"entries": [
{
"attributes": {
"cn": [
"Laura Barnes (lb3dp)"
],
"displayName": "Laura Barnes",
"givenName": [
"Laura"
],
"mail": [
"lb3dp@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E0:Associate Professor of Systems and Information Engineering"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Sys and Environment"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
},
"dn": "uid=lb3dp,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
"Laura Barnes (lb3dp)"
],
"displayName": [
"Laura Barnes"
],
"givenName": [
"Laura"
],
"mail": [
"lb3dp@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E0:Associate Professor of Systems and Information Engineering"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Sys and Environment"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
}
},
{
"attributes": {
"cn": [
"Dan Funk (dhf8r)"
],
"displayName": "Dan Funk",
"givenName": [
"Dan"
],
"mail": [
"dhf8r@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E42:He's a hoopy frood"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Study of Parallel Universes"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
},
"dn": "uid=dhf8r,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
"Dan Funk (dhf84)"
],
"displayName": [
"Dan Funk"
],
"givenName": [
"Dan"
],
"mail": [
"dhf8r@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E42:He's a hoopy frood"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Study of Parallel Universes"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
}
}
]
"entries": [
{
"dn": "uid=lb3dp,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
"Laura Barnes (lb3dp)"
],
"displayName": [
"Laura Barnes"
],
"givenName": [
"Laura"
],
"mail": [
"lb3dp@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E0:Associate Professor of Systems and Information Engineering"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Sys and Environment"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
}
},
{
"dn": "uid=dhf8r,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
"Dan Funk (dhf84)"
],
"displayName": [
"Dan Funk"
],
"givenName": [
"Dan"
],
"mail": [
"dhf8r@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E42:He's a hoopy frood"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Study of Parallel Universes"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
}
},
{
"dn": "uid=lje5u,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
"Elder, Lori J (lje5u)"
],
"displayName": [
"Lori Elder"
],
"givenName": [
"Lori"
],
"mail": [
"lje5u@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E42:The vision"
],
"uvaDisplayDepartment": [
"E0:EN-Phy Anything could go here."
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
}
}
]
}

View File

@ -0,0 +1,45 @@
<?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_1v9xfjq" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="looping_task" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0vlor2k</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="GetNames" name="Get Names" camunda:formKey="GetNamesForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="GetNames_CurrentVar.Name" type="string" />
<camunda:formField id="GetNames_CurrentVar.Nickname" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0vlor2k</bpmn:incoming>
<bpmn:outgoing>Flow_1tvod7v</bpmn:outgoing>
<bpmn:standardLoopCharacteristics />
</bpmn:userTask>
<bpmn:endEvent id="Event_End">
<bpmn:incoming>Flow_1tvod7v</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1tvod7v" sourceRef="GetNames" targetRef="Event_End" />
<bpmn:sequenceFlow id="Flow_0vlor2k" sourceRef="StartEvent_1" targetRef="GetNames" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="looping_task">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1mpvzb7_di" bpmnElement="GetNames">
<dc:Bounds x="250" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0dhdik5_di" bpmnElement="Event_End">
<dc:Bounds x="402" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1tvod7v_di" bpmnElement="Flow_1tvod7v">
<di:waypoint x="350" y="117" />
<di:waypoint x="402" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vlor2k_di" bpmnElement="Flow_0vlor2k">
<di:waypoint x="215" y="117" />
<di:waypoint x="250" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="MultiInstance" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" name="StartEvent_1">
<bpmn:outgoing>Flow_0t6p1sb</bpmn:outgoing>
@ -8,8 +8,8 @@
<bpmn:endEvent id="Event_End" name="Event_End">
<bpmn:incoming>Flow_0ugjw69</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0ugjw69" sourceRef="MutiInstanceTask" targetRef="Event_End" />
<bpmn:userTask id="MutiInstanceTask" name="Gather more information" camunda:formKey="GetEmail">
<bpmn:sequenceFlow id="Flow_0ugjw69" sourceRef="MultiInstanceTask" targetRef="Event_End" />
<bpmn:userTask id="MultiInstanceTask" name="Gather more information" camunda:formKey="GetEmail">
<bpmn:documentation># Please provide addtional information about:
## Investigator ID: {{investigator.NETBADGEID}}
## Role: {{investigator.INVESTIGATORTYPEFULL}}</bpmn:documentation>
@ -25,11 +25,11 @@
<bpmn:outgoing>Flow_0ugjw69</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics isSequential="true" camunda:collection="StudyInfo.investigators" camunda:elementVariable="investigator" />
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1p568pp" sourceRef="Task_1v0e2zu" targetRef="MutiInstanceTask" />
<bpmn:sequenceFlow id="SequenceFlow_1p568pp" sourceRef="Task_1v0e2zu" targetRef="MultiInstanceTask" />
<bpmn:scriptTask id="Task_1v0e2zu" name="Load Personnel">
<bpmn:incoming>Flow_0t6p1sb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1p568pp</bpmn:outgoing>
<bpmn:script>StudyInfo investigators</bpmn:script>
<bpmn:script>#! StudyInfo investigators</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
@ -58,7 +58,7 @@
<dc:Bounds x="585" y="142" width="54" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1iyilui_di" bpmnElement="MutiInstanceTask">
<bpmndi:BPMNShape id="Activity_1iyilui_di" bpmnElement="MultiInstanceTask">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_0cbbirp_di" bpmnElement="Task_1v0e2zu">

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="MultiInstance" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" name="StartEvent_1">
<bpmn:outgoing>Flow_0t6p1sb</bpmn:outgoing>
@ -8,8 +8,8 @@
<bpmn:endEvent id="Event_End" name="Event_End">
<bpmn:incoming>Flow_0ugjw69</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0ugjw69" sourceRef="MutiInstanceTask" targetRef="Event_End" />
<bpmn:userTask id="MutiInstanceTask" name="Gather more information" camunda:formKey="GetEmail">
<bpmn:sequenceFlow id="Flow_0ugjw69" sourceRef="MultiInstanceTask" targetRef="Event_End" />
<bpmn:userTask id="MultiInstanceTask" name="Gather more information" camunda:formKey="GetEmail">
<bpmn:documentation># Please provide addtional information about:
## Investigator ID: {{investigator.user_id}}
## Role: {{investigator.type_full}}</bpmn:documentation>
@ -17,16 +17,19 @@
<camunda:formData>
<camunda:formField id="email" label="Email Address:" type="string" />
</camunda:formData>
<camunda:properties>
<camunda:property name="display_name" value="investigator.label" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1p568pp</bpmn:incoming>
<bpmn:outgoing>Flow_0ugjw69</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics camunda:collection="StudyInfo.investigators" camunda:elementVariable="investigator" />
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1p568pp" sourceRef="Task_1v0e2zu" targetRef="MutiInstanceTask" />
<bpmn:sequenceFlow id="SequenceFlow_1p568pp" sourceRef="Task_1v0e2zu" targetRef="MultiInstanceTask" />
<bpmn:scriptTask id="Task_1v0e2zu" name="Load Personnel">
<bpmn:incoming>Flow_0t6p1sb</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1p568pp</bpmn:outgoing>
<bpmn:script>StudyInfo investigators</bpmn:script>
<bpmn:script>#! StudyInfo investigators</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
@ -55,7 +58,7 @@
<dc:Bounds x="575" y="142" width="54" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1iyilui_di" bpmnElement="MutiInstanceTask">
<bpmndi:BPMNShape id="Activity_1iyilui_di" bpmnElement="MultiInstanceTask">
<dc:Bounds x="410" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_0cbbirp_di" bpmnElement="Task_1v0e2zu">

View File

@ -13,5 +13,15 @@
"INVESTIGATORTYPE": "PI",
"INVESTIGATORTYPEFULL": "Primary Investigator",
"NETBADGEID": "dhf8r"
},
{
"INVESTIGATORTYPE": "SI",
"INVESTIGATORTYPEFULL": "Sub Investigator",
"NETBADGEID": "ajl2j"
},
{
"INVESTIGATORTYPE": "SI",
"INVESTIGATORTYPEFULL": "Sub Investigator",
"NETBADGEID": "cah3us"
}
]
]

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1gjhqt9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<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_1gjhqt9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_1ds61df" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0c7wlth</bpmn:outgoing>
@ -132,7 +132,7 @@ Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0641sh6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0t29gjo</bpmn:outgoing>
<bpmn:script>FactService</bpmn:script>
<bpmn:script>#! FactService</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0u1cgrf">
<bpmn:documentation># Great Job!
@ -175,9 +175,6 @@ Your random fact is:
<bpmndi:BPMNShape id="UserTask_186s7tw_di" bpmnElement="Task_User_Select_Type">
<dc:Bounds x="270" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_10keafb_di" bpmnElement="Task_Get_Fact_From_API">
<dc:Bounds x="470" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
<dc:Bounds x="692" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
@ -187,6 +184,9 @@ Your random fact is:
<bpmndi:BPMNShape id="TextAnnotation_1234e5n_di" bpmnElement="TextAnnotation_1234e5n">
<dc:Bounds x="570" y="120" width="100" height="68" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_10keafb_di" bpmnElement="Task_Get_Fact_From_API">
<dc:Bounds x="470" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1cfasjp_di" bpmnElement="Association_1cfasjp">
<di:waypoint x="344" y="210" />
<di:waypoint x="359" y="184" />

155
tests/data/roles/roles.bpmn Normal file
View File

@ -0,0 +1,155 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0ybr9ph" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.0.0">
<bpmn:collaboration id="Collaboration_0xjb3la">
<bpmn:participant id="Participant_0ozb2sp" name="pool_name" processRef="Process_1aebbrh" />
</bpmn:collaboration>
<bpmn:process id="Process_1aebbrh" isExecutable="true">
<bpmn:laneSet id="LaneSet_0ilprw6">
<bpmn:lane id="Lane_1s1s7a1">
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_1hljoeq</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Event_0lscajc</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_19ccxoj</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="Lane_1m47545" name="supervisor">
<bpmn:flowNodeRef>Gateway_1fkgc4u</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_14eor1x</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0a7090c</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Activity_1hljoeq" name="Request Approval" camunda:formKey="request_form_key">
<bpmn:documentation># Answer me these questions 3, ere the other side you see!</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="favorite_color" label="What is your favorite color?" type="string" defaultValue="Yellow" />
<camunda:formField id="quest" label="What is your quest?" type="string" defaultValue="To seek the holly Grail!" />
<camunda:formField id="swallow_speed" label="What is the air speed velocity of an unladen swallow?" type="string" defaultValue="About 24 miles per hour" />
<camunda:formField id="supervisor" label="Please enter the UVA Id of your supervisor" type="string" defaultValue="dhf8r" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0a7090c</bpmn:incoming>
<bpmn:incoming>Flow_070gq5r</bpmn:incoming>
<bpmn:outgoing>Flow_1hcpt7c</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_1fkgc4u">
<bpmn:incoming>Flow_1gp4zfd</bpmn:incoming>
<bpmn:outgoing>Flow_0vnghsi</bpmn:outgoing>
<bpmn:outgoing>Flow_1g38q6b</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="Event_0lscajc">
<bpmn:documentation># Your responses were approved!
Gosh! you must really know a lot about colors and swallows and stuff!
Your supervisor provided the following feedback:
{{feedback}}
You are all done! WARNING: If you go back and reanswer the questions it will create a new approval request.</bpmn:documentation>
<bpmn:incoming>Flow_1g38q6b</bpmn:incoming>
</bpmn:endEvent>
<bpmn:manualTask id="Activity_19ccxoj" name="Review Feedback">
<bpmn:documentation># Your Request was rejected
Perhaps you don't know the right answer to one of the questions.
Your Supervisor provided the following feedback:
{{feedback}}
Please press save to re-try the questions, and submit your responses again.</bpmn:documentation>
<bpmn:incoming>Flow_0vnghsi</bpmn:incoming>
<bpmn:outgoing>Flow_070gq5r</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0a7090c" sourceRef="StartEvent_1" targetRef="Activity_1hljoeq" />
<bpmn:sequenceFlow id="Flow_1gp4zfd" sourceRef="Activity_14eor1x" targetRef="Gateway_1fkgc4u" />
<bpmn:sequenceFlow id="Flow_0vnghsi" name="rejected" sourceRef="Gateway_1fkgc4u" targetRef="Activity_19ccxoj">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==False</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1g38q6b" name="approved" sourceRef="Gateway_1fkgc4u" targetRef="Event_0lscajc">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1hcpt7c" sourceRef="Activity_1hljoeq" targetRef="Activity_14eor1x" />
<bpmn:sequenceFlow id="Flow_070gq5r" sourceRef="Activity_19ccxoj" targetRef="Activity_1hljoeq" />
<bpmn:userTask id="Activity_14eor1x" name="Approve Responses" camunda:formKey="approval_form_key">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="approval" label="I approve of this information" type="boolean" />
<camunda:formField id="feedback" label="Feedback" type="string" defaultValue="Please provide any feedback you have here." />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1hcpt7c</bpmn:incoming>
<bpmn:outgoing>Flow_1gp4zfd</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0xjb3la">
<bpmndi:BPMNShape id="Participant_0ozb2sp_di" bpmnElement="Participant_0ozb2sp" isHorizontal="true">
<dc:Bounds x="190" y="80" width="550" height="310" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1m47545_di" bpmnElement="Lane_1m47545" isHorizontal="true">
<dc:Bounds x="220" y="265" width="520" height="125" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1s1s7a1_di" bpmnElement="Lane_1s1s7a1" isHorizontal="true">
<dc:Bounds x="220" y="80" width="520" height="185" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_070gq5r_di" bpmnElement="Flow_070gq5r">
<di:waypoint x="510" y="160" />
<di:waypoint x="510" y="100" />
<di:waypoint x="380" y="100" />
<di:waypoint x="380" y="160" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hcpt7c_di" bpmnElement="Flow_1hcpt7c">
<di:waypoint x="380" y="240" />
<di:waypoint x="380" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1g38q6b_di" bpmnElement="Flow_1g38q6b">
<di:waypoint x="535" y="330" />
<di:waypoint x="680" y="330" />
<di:waypoint x="680" y="218" />
<bpmndi:BPMNLabel>
<dc:Bounds x="585" y="312" width="46" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vnghsi_di" bpmnElement="Flow_0vnghsi">
<di:waypoint x="510" y="305" />
<di:waypoint x="510" y="240" />
<bpmndi:BPMNLabel>
<dc:Bounds x="520" y="274" width="40" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gp4zfd_di" bpmnElement="Flow_1gp4zfd">
<di:waypoint x="430" y="330" />
<di:waypoint x="485" y="330" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0a7090c_di" bpmnElement="Flow_0a7090c">
<di:waypoint x="276" y="200" />
<di:waypoint x="330" y="200" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="240" y="182" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0xcxw40_di" bpmnElement="Activity_1hljoeq">
<dc:Bounds x="330" y="160" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1fkgc4u_di" bpmnElement="Gateway_1fkgc4u" isMarkerVisible="true">
<dc:Bounds x="485" y="305" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0lscajc_di" bpmnElement="Event_0lscajc">
<dc:Bounds x="662" y="182" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1jfdeta_di" bpmnElement="Activity_19ccxoj">
<dc:Bounds x="460" y="160" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zc7cgy_di" bpmnElement="Activity_14eor1x">
<dc:Bounds x="330" y="290" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0exnnpv" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1nfe5m9</bpmn:outgoing>
@ -8,7 +8,7 @@
<bpmn:scriptTask id="Task_Script_Load_Study_Details" name="Load Study Info">
<bpmn:incoming>SequenceFlow_1nfe5m9</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1bqiin0</bpmn:outgoing>
<bpmn:script>StudyInfo info</bpmn:script>
<bpmn:script>#! StudyInfo info</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1bqiin0" sourceRef="Task_Script_Load_Study_Details" targetRef="EndEvent_171dj09" />
<bpmn:endEvent id="EndEvent_171dj09">

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1kudwnk" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<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_1kudwnk" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0jhpidf" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1ees8ka</bpmn:outgoing>
@ -11,7 +11,7 @@
<bpmn:scriptTask id="Task_Load_Requirements" name="Load Required Documents From PM">
<bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>StudyInfo documents</bpmn:script>
<bpmn:script>#! StudyInfo documents</bpmn:script>
</bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<bpmn:incoming>Flow_1m8285h</bpmn:incoming>

View File

@ -0,0 +1,39 @@
from tests.base_test import BaseTest
from crc.models.email import EmailModel
from crc.services.file_service import FileService
from crc.scripts.email import Email
from crc.services.workflow_processor import WorkflowProcessor
from crc.api.common import ApiError
from crc import db, mail
class TestEmailScript(BaseTest):
def test_do_task(self):
workflow = self.create_workflow('email')
task_data = {
'PIComputingID': 'dhf8r',
'ApprvlApprvr1': 'lb3dp'
}
task = self.get_workflow_api(workflow).next_task
with mail.record_messages() as outbox:
self.complete_form(workflow, task, task_data)
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].subject, 'Camunda Email Subject')
# PI is present
self.assertIn(task_data['PIComputingID'], outbox[0].body)
self.assertIn(task_data['PIComputingID'], outbox[0].html)
# Approver is present
self.assertIn(task_data['ApprvlApprvr1'], outbox[0].body)
self.assertIn(task_data['ApprvlApprvr1'], outbox[0].html)
db_emails = EmailModel.query.count()
self.assertEqual(db_emails, 1)

View File

@ -0,0 +1,45 @@
from tests.base_test import BaseTest
from crc import session
from crc.models.approval import ApprovalModel, ApprovalStatus
from crc.models.email import EmailModel
from crc.services.email_service import EmailService
class TestEmailService(BaseTest):
def test_add_email(self):
self.load_example_data()
study = self.create_study()
workflow = self.create_workflow('random_fact')
subject = 'Email Subject'
sender = 'sender@sartography.com'
recipients = ['recipient@sartography.com', 'back@sartography.com']
content = 'Content for this email'
content_html = '<p>Hypertext Markup Language content for this email</p>'
EmailService.add_email(subject=subject, sender=sender, recipients=recipients,
content=content, content_html=content_html, study_id=study.id)
email_model = EmailModel.query.first()
self.assertEqual(email_model.subject, subject)
self.assertEqual(email_model.sender, sender)
self.assertEqual(email_model.recipients, str(recipients))
self.assertEqual(email_model.content, content)
self.assertEqual(email_model.content_html, content_html)
self.assertEqual(email_model.study, study)
subject = 'Email Subject - Empty study'
EmailService.add_email(subject=subject, sender=sender, recipients=recipients,
content=content, content_html=content_html)
email_model = EmailModel.query.order_by(EmailModel.id.desc()).first()
self.assertEqual(email_model.subject, subject)
self.assertEqual(email_model.sender, sender)
self.assertEqual(email_model.recipients, str(recipients))
self.assertEqual(email_model.content, content)
self.assertEqual(email_model.content_html, content_html)
self.assertEqual(email_model.study, None)

113
tests/emails/test_mails.py Normal file
View File

@ -0,0 +1,113 @@
from crc import mail
from crc.models.email import EmailModel
from crc.services.mails import (
send_ramp_up_submission_email,
send_ramp_up_approval_request_email,
send_ramp_up_approval_request_first_review_email,
send_ramp_up_approved_email,
send_ramp_up_denied_email,
send_ramp_up_denied_email_to_approver
)
from tests.base_test import BaseTest
class TestMails(BaseTest):
def setUp(self):
"""Initial setup shared by all TestApprovals tests"""
self.load_example_data()
self.study = self.create_study()
self.workflow = self.create_workflow('random_fact')
self.sender = 'sender@sartography.com'
self.recipients = ['recipient@sartography.com']
self.primary_investigator = 'Dr. Bartlett'
self.approver_1 = 'Max Approver'
self.approver_2 = 'Close Reviewer'
def test_send_ramp_up_submission_email(self):
with mail.record_messages() as outbox:
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1)
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Submitted')
self.assertIn(self.approver_1, outbox[0].body)
self.assertIn(self.approver_1, outbox[0].html)
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1, self.approver_2)
self.assertEqual(len(outbox), 2)
self.assertIn(self.approver_1, outbox[1].body)
self.assertIn(self.approver_1, outbox[1].html)
self.assertIn(self.approver_2, outbox[1].body)
self.assertIn(self.approver_2, outbox[1].html)
db_emails = EmailModel.query.count()
self.assertEqual(db_emails, 2)
def test_send_ramp_up_approval_request_email(self):
with mail.record_messages() as outbox:
send_ramp_up_approval_request_email(self.sender, self.recipients, self.primary_investigator)
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Approval Request')
self.assertIn(self.primary_investigator, outbox[0].body)
self.assertIn(self.primary_investigator, outbox[0].html)
db_emails = EmailModel.query.count()
self.assertEqual(db_emails, 1)
def test_send_ramp_up_approval_request_first_review_email(self):
with mail.record_messages() as outbox:
send_ramp_up_approval_request_first_review_email(
self.sender, self.recipients, self.primary_investigator
)
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Approval Request')
self.assertIn(self.primary_investigator, outbox[0].body)
self.assertIn(self.primary_investigator, outbox[0].html)
db_emails = EmailModel.query.count()
self.assertEqual(db_emails, 1)
def test_send_ramp_up_approved_email(self):
with mail.record_messages() as outbox:
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1)
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Approved')
self.assertIn(self.approver_1, outbox[0].body)
self.assertIn(self.approver_1, outbox[0].html)
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1, self.approver_2)
self.assertEqual(len(outbox), 2)
self.assertIn(self.approver_1, outbox[1].body)
self.assertIn(self.approver_1, outbox[1].html)
self.assertIn(self.approver_2, outbox[1].body)
self.assertIn(self.approver_2, outbox[1].html)
db_emails = EmailModel.query.count()
self.assertEqual(db_emails, 2)
def test_send_ramp_up_denied_email(self):
with mail.record_messages() as outbox:
send_ramp_up_denied_email(self.sender, self.recipients, self.approver_1)
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Denied')
self.assertIn(self.approver_1, outbox[0].body)
self.assertIn(self.approver_1, outbox[0].html)
db_emails = EmailModel.query.count()
self.assertEqual(db_emails, 1)
def test_send_send_ramp_up_denied_email_to_approver(self):
with mail.record_messages() as outbox:
send_ramp_up_denied_email_to_approver(
self.sender, self.recipients, self.primary_investigator, self.approver_2
)
self.assertEqual(outbox[0].subject, 'Research Ramp-up Plan Denied')
self.assertIn(self.primary_investigator, outbox[0].body)
self.assertIn(self.primary_investigator, outbox[0].html)
self.assertIn(self.approver_2, outbox[0].body)
self.assertIn(self.approver_2, outbox[0].html)
db_emails = EmailModel.query.count()
self.assertEqual(db_emails, 1)

View File

@ -61,14 +61,14 @@ class TestFileService(BaseTest):
# Archive the file
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
self.assertEquals(1, len(file_models))
self.assertEqual(1, len(file_models))
file_model = file_models[0]
file_model.archived = True
db.session.add(file_model)
# Assure that the file no longer comes back.
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
self.assertEquals(0, len(file_models))
self.assertEqual(0, len(file_models))
# Add the file again with different data
FileService.add_workflow_file(workflow_id=workflow.id,

View File

@ -91,7 +91,6 @@ class TestFilesApi(BaseTest):
content_type='multipart/form-data', headers=self.logged_in_headers())
self.assert_success(rv)
def test_archive_file_no_longer_shows_up(self):
self.load_example_data()
self.create_reference_document()
@ -109,21 +108,16 @@ class TestFilesApi(BaseTest):
self.assert_success(rv)
rv = self.app.get('/v1.0/file?workflow_id=%s' % workflow.id, headers=self.logged_in_headers())
self.assert_success(rv)
self.assertEquals(1, len(json.loads(rv.get_data(as_text=True))))
self.assertEqual(1, len(json.loads(rv.get_data(as_text=True))))
file_model = db.session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all()
self.assertEquals(1, len(file_model))
self.assertEqual(1, len(file_model))
file_model[0].archived = True
db.session.commit()
rv = self.app.get('/v1.0/file?workflow_id=%s' % workflow.id, headers=self.logged_in_headers())
self.assert_success(rv)
self.assertEquals(0, len(json.loads(rv.get_data(as_text=True))))
self.assertEqual(0, len(json.loads(rv.get_data(as_text=True))))
def test_set_reference_file(self):
file_name = "irb_document_types.xls"
@ -285,8 +279,8 @@ class TestFilesApi(BaseTest):
.filter(ApprovalModel.status == ApprovalStatus.PENDING.value)\
.filter(ApprovalModel.study_id == workflow.study_id).all()
self.assertEquals(1, len(approvals))
self.assertEquals(1, len(approvals[0].approval_files))
self.assertEqual(1, len(approvals))
self.assertEqual(1, len(approvals[0].approval_files))
def test_change_primary_bpmn(self):

View File

@ -7,7 +7,8 @@ from unittest.mock import patch
from crc import session, app
from crc.models.protocol_builder import ProtocolBuilderStatus, \
ProtocolBuilderStudySchema
from crc.models.stats import TaskEventModel
from crc.models.approval import ApprovalStatus
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel, StudySchema
from crc.models.workflow import WorkflowSpecModel, WorkflowModel
from crc.services.file_service import FileService
@ -95,8 +96,21 @@ class TestStudyApi(BaseTest):
# TODO: WRITE A TEST FOR STUDY FILES
def test_get_study_has_details_about_approvals(self):
# TODO: WRITE A TEST FOR STUDY APPROVALS
pass
self.load_example_data()
full_study = self._create_study_workflow_approvals(
user_uid="dhf8r", title="first study", primary_investigator_id="lb3dp",
approver_uids=["lb3dp", "dhf8r"], statuses=[ApprovalStatus.PENDING.value, ApprovalStatus.PENDING.value]
)
api_response = self.app.get('/v1.0/study/%i' % full_study['study'].id,
headers=self.logged_in_headers(), content_type="application/json")
self.assert_success(api_response)
study = StudySchema().loads(api_response.get_data(as_text=True))
self.assertEqual(len(study.approvals), 2)
for approval in study.approvals:
self.assertEqual(full_study['study'].title, approval['title'])
def test_add_study(self):
self.load_example_data()
@ -168,8 +182,6 @@ class TestStudyApi(BaseTest):
num_open = 0
for study in json_data:
if study['protocol_builder_status'] == 'INCOMPLETE': # One study in user_studies.json is not q_complete
num_incomplete += 1
if study['protocol_builder_status'] == 'ABANDONED': # One study does not exist in user_studies.json
num_abandoned += 1
if study['protocol_builder_status'] == 'ACTIVE': # One study is marked complete without HSR Number
@ -182,8 +194,8 @@ class TestStudyApi(BaseTest):
self.assertGreater(num_db_studies_after, num_db_studies_before)
self.assertEqual(num_abandoned, 1)
self.assertEqual(num_open, 1)
self.assertEqual(num_active, 1)
self.assertEqual(num_incomplete, 1)
self.assertEqual(num_active, 2)
self.assertEqual(num_incomplete, 0)
self.assertEqual(len(json_data), num_db_studies_after)
self.assertEqual(num_open + num_active + num_incomplete + num_abandoned, num_db_studies_after)

View File

@ -183,7 +183,7 @@ class TestStudyService(BaseTest):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_docs
def test_get_personnel(self, mock_docs):
def test_get_personnel_roles(self, mock_docs):
self.load_example_data()
# mock out the protocol builder
@ -191,9 +191,9 @@ class TestStudyService(BaseTest):
mock_docs.return_value = json.loads(docs_response)
workflow = self.create_workflow('docx') # The workflow really doesnt matter in this case.
investigators = StudyService().get_investigators(workflow.study_id)
investigators = StudyService().get_investigators(workflow.study_id, all=True)
self.assertEqual(9, len(investigators))
self.assertEqual(10, len(investigators))
# dhf8r is in the ldap mock data.
self.assertEqual("dhf8r", investigators['PI']['user_id'])
@ -207,3 +207,26 @@ class TestStudyService(BaseTest):
# No value is provided for Department Chair
self.assertIsNone(investigators['DEPT_CH']['user_id'])
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_docs
def test_get_study_personnel(self, mock_docs):
self.load_example_data()
# mock out the protocol builder
docs_response = self.protocol_builder_response('investigators.json')
mock_docs.return_value = json.loads(docs_response)
workflow = self.create_workflow('docx') # The workflow really doesnt matter in this case.
investigators = StudyService().get_investigators(workflow.study_id, all=False)
self.assertEqual(5, len(investigators))
# dhf8r is in the ldap mock data.
self.assertEqual("dhf8r", investigators['PI']['user_id'])
self.assertEqual("Dan Funk", investigators['PI']['display_name']) # Data from ldap
self.assertEqual("Primary Investigator", investigators['PI']['label']) # Data from xls file.
self.assertEqual("Always", investigators['PI']['display']) # Data from xls file.
# Both Alex and Aaron are SI, and both should be returned.
self.assertEqual("ajl2j", investigators['SI']['user_id'])
self.assertEqual("cah3us", investigators['SI_2']['user_id'])

View File

@ -61,6 +61,15 @@ class TestLookupService(BaseTest):
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
self.assertEqual(4, len(lookup_data))
def test_lookup_based_on_id(self):
spec = BaseTest.load_test_spec('enum_options_from_file')
workflow = self.create_workflow('enum_options_from_file')
processor = WorkflowProcessor(workflow)
processor.do_engine_steps()
results = LookupService.lookup(workflow, "AllTheNames", "", value="1000", limit=10)
self.assertEqual(1, len(results), "It is possible to find an item based on the id, rather than as a search")
self.assertIsNotNone(results[0].data)
self.assertIsInstance(results[0].data, dict)
def test_some_full_text_queries(self):
@ -114,6 +123,9 @@ class TestLookupService(BaseTest):
results = LookupService.lookup(workflow, "AllTheNames", "1 (!-Something", limit=10)
self.assertEqual("1 Something", results[0].label, "special characters don't flake out")
results = LookupService.lookup(workflow, "AllTheNames", "1 Something", limit=10)
self.assertEqual("1 Something", results[0].label, "double spaces should not be an issue.")
# 1018 10000 Something Industry

View File

@ -0,0 +1,54 @@
from unittest.mock import patch
from crc import session
from crc.models.api_models import MultiInstanceType
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowStatus
from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
class TestWorkflowProcessorLoopingTask(BaseTest):
"""Tests the Workflow Processor as it deals with a Looping task"""
def _populate_form_with_random_data(self, task):
api_task = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True)
WorkflowService.populate_form_with_random_data(task, api_task, required_only=False)
def get_processor(self, study_model, spec_model):
workflow_model = StudyService._create_workflow_model(study_model, spec_model)
return WorkflowProcessor(workflow_model)
def test_create_and_complete_workflow(self):
# This depends on getting a list of investigators back from the protocol builder.
workflow = self.create_workflow('looping_task')
task = self.get_workflow_api(workflow).next_task
self.assertEqual("GetNames", task.name)
self.assertEqual(task.multi_instance_type, 'looping')
self.assertEqual(1, task.multi_instance_index)
self.complete_form(workflow,task,{'GetNames_CurrentVar':{'Name': 'Peter Norvig', 'Nickname': 'Pete'}})
task = self.get_workflow_api(workflow).next_task
self.assertEqual(task.multi_instance_type,'looping')
self.assertEqual(2, task.multi_instance_index)
self.complete_form(workflow,
task,
{'GetNames_CurrentVar':{'Name': 'Stuart Russell', 'Nickname': 'Stu'}},
terminate_loop=True)
task = self.get_workflow_api(workflow).next_task
self.assertEqual(task.name,'Event_End')
self.assertEqual(workflow.completed_tasks,workflow.total_tasks)
self.assertEqual(task.data, {'GetNames_CurrentVar': 2,
'GetNames': {'1': {'Name': 'Peter Norvig',
'Nickname': 'Pete'},
'2': {'Name': 'Stuart Russell',
'Nickname': 'Stu'}}})

View File

@ -1,55 +0,0 @@
from tests.base_test import BaseTest
from crc.services.mails import (
send_ramp_up_submission_email,
send_ramp_up_approval_request_email,
send_ramp_up_approval_request_first_review_email,
send_ramp_up_approved_email,
send_ramp_up_denied_email,
send_ramp_up_denied_email_to_approver
)
class TestMails(BaseTest):
def setUp(self):
self.sender = 'sender@sartography.com'
self.recipients = ['recipient@sartography.com']
self.primary_investigator = 'Dr. Bartlett'
self.approver_1 = 'Max Approver'
self.approver_2 = 'Close Reviewer'
def test_send_ramp_up_submission_email(self):
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1)
self.assertTrue(True)
send_ramp_up_submission_email(self.sender, self.recipients, self.approver_1, self.approver_2)
self.assertTrue(True)
def test_send_ramp_up_approval_request_email(self):
send_ramp_up_approval_request_email(self.sender, self.recipients, self.primary_investigator)
self.assertTrue(True)
def test_send_ramp_up_approval_request_first_review_email(self):
send_ramp_up_approval_request_first_review_email(
self.sender, self.recipients, self.primary_investigator
)
self.assertTrue(True)
def test_send_ramp_up_approved_email(self):
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1)
self.assertTrue(True)
send_ramp_up_approved_email(self.sender, self.recipients, self.approver_1, self.approver_2)
self.assertTrue(True)
def test_send_ramp_up_denied_email(self):
send_ramp_up_denied_email(self.sender, self.recipients, self.approver_1)
self.assertTrue(True)
def test_send_send_ramp_up_denied_email_to_approver(self):
send_ramp_up_denied_email_to_approver(
self.sender, self.recipients, self.primary_investigator, self.approver_2
)
self.assertTrue(True)

View File

@ -24,7 +24,7 @@ class TestProtocolBuilder(BaseTest):
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
response = ProtocolBuilderService.get_investigators(self.test_study_id)
self.assertIsNotNone(response)
self.assertEqual(3, len(response))
self.assertEqual(5, len(response))
self.assertEqual("DC", response[0]["INVESTIGATORTYPE"])
self.assertEqual("Department Contact", response[0]["INVESTIGATORTYPEFULL"])
self.assertEqual("asd3v", response[0]["NETBADGEID"])

View File

@ -4,6 +4,7 @@ import random
from unittest.mock import patch
from tests.base_test import BaseTest
from crc import session, app
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType, TaskSchema
from crc.models.file import FileModelSchema
@ -12,6 +13,18 @@ from crc.models.workflow import WorkflowStatus
class TestTasksApi(BaseTest):
def assert_options_populated(self, results, lookup_data_keys):
option_keys = ['value', 'label', 'data']
self.assertIsInstance(results, list)
for result in results:
for option_key in option_keys:
self.assertTrue(option_key in result, 'should have value, label, and data properties populated')
self.assertIsNotNone(result[option_key], '%s should not be None' % option_key)
self.assertIsInstance(result['data'], dict)
for lookup_data_key in lookup_data_keys:
self.assertTrue(lookup_data_key in result['data'], 'should have all lookup data columns populated')
def test_get_current_user_tasks(self):
self.load_example_data()
workflow = self.create_workflow('random_fact')
@ -250,7 +263,7 @@ class TestTasksApi(BaseTest):
self.assertEqual(4, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual("UserTask", workflow.next_task.type)
self.assertEqual(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
self.assertEqual(9, workflow.next_task.multi_instance_count)
self.assertEqual(5, workflow.next_task.multi_instance_count)
# Assure that the names for each task are properly updated, so they aren't all the same.
self.assertEqual("Primary Investigator", workflow.next_task.properties['display_name'])
@ -270,6 +283,80 @@ class TestTasksApi(BaseTest):
self.assert_success(rv)
results = json.loads(rv.get_data(as_text=True))
self.assertEqual(5, len(results))
self.assert_options_populated(results, ['CUSTOMER_NUMBER', 'CUSTOMER_NAME', 'CUSTOMER_CLASS_MEANING'])
def test_lookup_endpoint_for_task_field_using_lookup_entry_id(self):
self.load_example_data()
workflow = self.create_workflow('enum_options_with_search')
# get the first form in the two form workflow.
workflow = self.get_workflow_api(workflow)
task = workflow.next_task
field_id = task.form['fields'][0]['id']
rv = self.app.get('/v1.0/workflow/%i/lookup/%s?query=%s&limit=5' %
(workflow.id, field_id, 'c'), # All records with a word that starts with 'c'
headers=self.logged_in_headers(),
content_type="application/json")
self.assert_success(rv)
results = json.loads(rv.get_data(as_text=True))
self.assertEqual(5, len(results))
self.assert_options_populated(results, ['CUSTOMER_NUMBER', 'CUSTOMER_NAME', 'CUSTOMER_CLASS_MEANING'])
rv = self.app.get('/v1.0/workflow/%i/lookup/%s?value=%s' %
(workflow.id, field_id, results[0]['value']), # All records with a word that starts with 'c'
headers=self.logged_in_headers(),
content_type="application/json")
results = json.loads(rv.get_data(as_text=True))
self.assertEqual(1, len(results))
self.assert_options_populated(results, ['CUSTOMER_NUMBER', 'CUSTOMER_NAME', 'CUSTOMER_CLASS_MEANING'])
self.assertNotIn('id', results[0], "Don't include the internal id, that can be very confusing, and should not be used.")
def test_lookup_endpoint_also_works_for_enum(self):
# Naming here get's a little confusing. fields can be marked as enum or autocomplete.
# In the event of an auto-complete it's a type-ahead search field, for an enum the
# the key/values from the spreadsheet are added directly to the form and it shows up as
# a dropdown. This tests the case of wanting to get additional data when a user selects
# something from a dropdown.
self.load_example_data()
workflow = self.create_workflow('enum_options_from_file')
# get the first form in the two form workflow.
workflow = self.get_workflow_api(workflow)
task = workflow.next_task
field_id = task.form['fields'][0]['id']
option_id = task.form['fields'][0]['options'][0]['id']
rv = self.app.get('/v1.0/workflow/%i/lookup/%s?value=%s' %
(workflow.id, field_id, option_id), # All records with a word that starts with 'c'
headers=self.logged_in_headers(),
content_type="application/json")
self.assert_success(rv)
results = json.loads(rv.get_data(as_text=True))
self.assertEqual(1, len(results))
self.assert_options_populated(results, ['CUSTOMER_NUMBER', 'CUSTOMER_NAME', 'CUSTOMER_CLASS_MEANING'])
self.assertIsInstance(results[0]['data'], dict)
def test_enum_from_task_data(self):
self.load_example_data()
workflow = self.create_workflow('enum_options_from_task_data')
# get the first form in the two form workflow.
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
workflow_api = self.complete_form(workflow, task, {'invitees': [
{'first_name': 'Alistair', 'last_name': 'Aardvark', 'age': 43, 'likes_pie': True, 'num_lumps': 21, 'secret_id': 'Antimony', 'display_name': 'Professor Alistair A. Aardvark'},
{'first_name': 'Berthilda', 'last_name': 'Binturong', 'age': 12, 'likes_pie': False, 'num_lumps': 34, 'secret_id': 'Beryllium', 'display_name': 'Dr. Berthilda B. Binturong'},
{'first_name': 'Chesterfield', 'last_name': 'Capybara', 'age': 32, 'likes_pie': True, 'num_lumps': 1, 'secret_id': 'Cadmium', 'display_name': 'The Honorable C. C. Capybara'},
]})
task = workflow_api.next_task
field_id = task.form['fields'][0]['id']
options = task.form['fields'][0]['options']
self.assertEqual(3, len(options))
option_id = options[0]['id']
self.assertEqual('Professor Alistair A. Aardvark', options[0]['name'])
self.assertEqual('Dr. Berthilda B. Binturong', options[1]['name'])
self.assertEqual('The Honorable C. C. Capybara', options[2]['name'])
self.assertEqual('Alistair', options[0]['data']['first_name'])
self.assertEqual('Berthilda', options[1]['data']['first_name'])
self.assertEqual('Chesterfield', options[2]['data']['first_name'])
def test_lookup_endpoint_for_task_ldap_field_lookup(self):
self.load_example_data()
@ -285,6 +372,9 @@ class TestTasksApi(BaseTest):
content_type="application/json")
self.assert_success(rv)
results = json.loads(rv.get_data(as_text=True))
self.assert_options_populated(results, ['telephone_number', 'affiliation', 'uid', 'title',
'given_name', 'department', 'date_cached', 'sponsor_type',
'display_name', 'email_address'])
self.assertEqual(1, len(results))
def test_sub_process(self):
@ -299,13 +389,13 @@ class TestTasksApi(BaseTest):
self.assertEqual("UserTask", task.type)
self.assertEqual("Activity_A", task.name)
self.assertEqual("My Sub Process", task.process_name)
workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
workflow_api = self.complete_form(workflow, task, {"FieldA": "Dan"})
task = workflow_api.next_task
self.assertIsNotNone(task)
self.assertEqual("Activity_B", task.name)
self.assertEqual("Sub Workflow Example", task.process_name)
workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
workflow_api = self.complete_form(workflow, task, {"FieldB": "Dan"})
self.assertEqual(WorkflowStatus.complete, workflow_api.status)
def test_update_task_resets_token(self):
@ -363,17 +453,25 @@ class TestTasksApi(BaseTest):
workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow)
self.assertEqual(12, len(workflow_api.navigation))
self.assertEqual(8, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav['state'] == "READY"]
self.assertEqual(9, len(ready_items))
self.assertEqual(5, len(ready_items))
self.assertEqual("UserTask", workflow_api.next_task.type)
self.assertEqual("MutiInstanceTask",workflow_api.next_task.name)
self.assertEqual("more information", workflow_api.next_task.title)
self.assertEqual("MultiInstanceTask",workflow_api.next_task.name)
self.assertEqual("Primary Investigator", workflow_api.next_task.title)
for i in random.sample(range(9), 9):
for i in random.sample(range(5), 5):
task = TaskSchema().load(ready_items[i]['task'])
self.complete_form(workflow, task, {"investigator":{"email": "dhf8r@virginia.edu"}})
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, task.id),
headers=self.logged_in_headers(),
content_type="application/json")
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
workflow = WorkflowApiSchema().load(json_data)
data = workflow.next_task.data
data['investigator']['email'] = "dhf8r@virginia.edu"
self.complete_form(workflow, task, data)
#tasks = self.get_workflow_api(workflow).user_tasks
workflow = self.get_workflow_api(workflow)

View File

@ -37,3 +37,12 @@ class TestStudyApi(BaseTest):
self.assertTrue(len(scripts) > 1)
self.assertIsNotNone(scripts[0]['name'])
self.assertIsNotNone(scripts[0]['description'])
def test_eval_hide_expression(self):
"""Assures we can use python to process a hide expression fron the front end"""
rv = self.app.put('/v1.0/eval?expression=x.y==2',
data='{"x":{"y":2}}', follow_redirects=True,
content_type='application/json',
headers=self.logged_in_headers())
self.assert_success(rv)
self.assertEqual("true", rv.get_data(as_text=True).strip())

202
tests/test_user_roles.py Normal file
View File

@ -0,0 +1,202 @@
import json
from tests.base_test import BaseTest
from crc.models.workflow import WorkflowStatus
from crc import db
from crc.api.common import ApiError
from crc.models.task_event import TaskEventModel, TaskEventSchema
from crc.services.workflow_service import WorkflowService
class TestTasksApi(BaseTest):
def test_raise_error_if_role_does_not_exist_in_data(self):
workflow = self.create_workflow('roles', as_user="lje5u")
workflow_api = self.get_workflow_api(workflow, user_uid="lje5u")
data = workflow_api.next_task.data
# User lje5u can complete the first task
self.complete_form(workflow, workflow_api.next_task, data, user_uid="lje5u")
# The next task is a supervisor task, and should raise an error if the role
# information is not in the task data.
workflow_api = self.get_workflow_api(workflow, user_uid="lje5u")
data = workflow_api.next_task.data
data["approved"] = True
result = self.complete_form(workflow, workflow_api.next_task, data, user_uid="lje5u",
error_code="permission_denied")
def test_validation_of_workflow_fails_if_workflow_does_not_define_user_for_lane(self):
error = None
try:
workflow = self.create_workflow('invalid_roles', as_user="lje5u")
WorkflowService.test_spec(workflow.workflow_spec_id)
except ApiError as ae:
error = ae
self.assertIsNotNone(error, "An error should be raised.")
self.assertEquals("invalid_role", error.code)
def test_raise_error_if_user_does_not_have_the_correct_role(self):
submitter = self.create_user(uid='lje5u')
supervisor = self.create_user(uid='lb3dp')
workflow = self.create_workflow('roles', as_user=submitter.uid)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
# User lje5u can complete the first task, and set her supervisor
data = workflow_api.next_task.data
data['supervisor'] = supervisor.uid
self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
# But she can not complete the supervisor role.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
data = workflow_api.next_task.data
data["approval"] = True
result = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid,
error_code="permission_denied")
# Only her supervisor can do that.
self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
def test_nav_includes_lanes(self):
submitter = self.create_user(uid='lje5u')
workflow = self.create_workflow('roles', as_user=submitter.uid)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals("supervisor", nav[1]['lane'])
def test_get_outstanding_tasks_awaiting_current_user(self):
submitter = self.create_user(uid='lje5u')
supervisor = self.create_user(uid='lb3dp')
workflow = self.create_workflow('roles', as_user=submitter.uid)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
# User lje5u can complete the first task, and set her supervisor
data = workflow_api.next_task.data
data['supervisor'] = supervisor.uid
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
# At this point there should be a task_log with an action of Lane Change on it for
# the supervisor.
task_logs = db.session.query(TaskEventModel). \
filter(TaskEventModel.user_uid == supervisor.uid). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).all()
self.assertEquals(1, len(task_logs))
# A call to the /task endpoint as the supervisor user should return a list of
# tasks that need their attention.
rv = self.app.get('/v1.0/task_events?action=ASSIGNMENT',
headers=self.logged_in_headers(supervisor),
content_type="application/json")
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
tasks = TaskEventSchema(many=True).load(json_data)
self.assertEquals(1, len(tasks))
self.assertEquals(workflow.id, tasks[0]['workflow']['id'])
self.assertEquals(workflow.study.id, tasks[0]['study']['id'])
# Assure we can say something sensible like:
# You have a task called "Approval" to be completed in the "Supervisor Approval" workflow
# for the study 'Why dogs are stinky' managed by user "Jane Smith (js42x)",
# please check here to complete the task.
# Display name isn't set in the tests, so just checking name, but the full workflow details are included.
# I didn't delve into the full user details to keep things decoupled from ldap, so you just get the
# uid back, but could query to get the full entry.
self.assertEquals("roles", tasks[0]['workflow']['name'])
self.assertEquals("Beer consumption in the bipedal software engineer", tasks[0]['study']['title'])
self.assertEquals("lje5u", tasks[0]['study']['user_uid'])
# Completing the next step of the workflow will close the task.
data['approval'] = True
self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
def test_navigation_and_current_task_updates_through_workflow(self):
submitter = self.create_user(uid='lje5u')
supervisor = self.create_user(uid='lb3dp')
workflow = self.create_workflow('roles', as_user=submitter.uid)
# Navigation as Submitter with ready task.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('READY', nav[0]['state']) # First item is ready, no progress yet.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('NOOP', nav[3]['state']) # Approved Path, has no operation
self.assertEquals('NOOP', nav[4]['state']) # Rejected Path, has no operation.
self.assertEquals('READY', workflow_api.next_task.state)
# Navigation as Submitter after handoff to supervisor
data = workflow_api.next_task.data
data['supervisor'] = supervisor.uid
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals('COMPLETED', nav[0]['state']) # First item is ready, no progress yet.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('LOCKED', workflow_api.next_task.state)
# In the event the next task is locked, we should say something sensible here.
# It is possible to look at the role of the task, and say The next task "TASK TITLE" will
# be handled by 'dhf8r', who is full-filling the role of supervisor. the Task Data
# is guaranteed to have a supervisor attribute in it that will contain the users uid, which
# could be looked up through an ldap service.
self.assertEquals('supervisor', workflow_api.next_task.lane)
# Navigation as Supervisor
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('READY', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
data = workflow_api.next_task.data
data["approval"] = False
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
# Navigation as Supervisor, after completing task.
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('COMPLETED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('COMPLETED', nav[2]['state']) # third item is a gateway, and is now complete.
self.assertEquals('LOCKED', workflow_api.next_task.state)
# Navigation as Submitter, coming back in to a rejected workflow to view the rejection message.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('COMPLETED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
# Navigation as Submitter, re-completing the original request a second time, and sending it for review.
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('COMPLETED', nav[0]['state']) # We still have some issues here, the navigation will be off when looping back.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
data["favorite_color"] = "blue"
data["quest"] = "to seek the holy grail"
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
self.assertEquals('LOCKED', workflow_api.next_task.state)
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
self.assertEquals('READY', workflow_api.next_task.state)
data = workflow_api.next_task.data
data["approval"] = True
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
self.assertEquals('LOCKED', workflow_api.next_task.state)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
self.assertEquals('COMPLETED', workflow_api.next_task.state)
self.assertEquals('EndEvent', workflow_api.next_task.type) # Are are at the end.
self.assertEquals(WorkflowStatus.complete, workflow_api.status)

View File

@ -187,7 +187,7 @@ class TestWorkflowProcessor(BaseTest):
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'two_forms', 'mods', 'two_forms_struc_mod.bpmn')
self.replace_file("two_forms.bpmn", file_path)
# Attemping a soft update on a structural change should raise a sensible error.
# Attempting a soft update on a structural change should raise a sensible error.
with self.assertRaises(ApiError) as context:
processor3 = WorkflowProcessor(processor.workflow_model, soft_reset=True)
self.assertEqual("unexpected_workflow_structure", context.exception.code)
@ -270,53 +270,6 @@ class TestWorkflowProcessor(BaseTest):
processor = self.get_processor(study, workflow_spec_model)
self.assertTrue(processor.get_version_string().startswith('v2.1.1'))
def test_restart_workflow(self):
self.load_example_data()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("two_forms")
processor = self.get_processor(study, workflow_spec_model)
self.assertEqual(processor.workflow_model.workflow_spec_id, workflow_spec_model.id)
task = processor.next_task()
task.data = {"key": "Value"}
processor.complete_task(task)
task_before_restart = processor.next_task()
processor.hard_reset()
task_after_restart = processor.next_task()
self.assertNotEqual(task.get_name(), task_before_restart.get_name())
self.assertEqual(task.get_name(), task_after_restart.get_name())
self.assertEqual(task.data, task_after_restart.data)
def test_soft_reset(self):
self.load_example_data()
# Start the two_forms workflow, and enter some data in the first form.
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("two_forms")
processor = self.get_processor(study, workflow_spec_model)
self.assertEqual(processor.workflow_model.workflow_spec_id, workflow_spec_model.id)
task = processor.next_task()
task.data = {"color": "blue"}
processor.complete_task(task)
# Modify the specification, with a minor text change.
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'two_forms', 'mods', 'two_forms_text_mod.bpmn')
self.replace_file("two_forms.bpmn", file_path)
# Setting up another processor should not error out, but doesn't pick up the update.
processor.workflow_model.bpmn_workflow_json = processor.serialize()
processor2 = WorkflowProcessor(processor.workflow_model)
self.assertEqual("Step 1", processor2.bpmn_workflow.last_task.task_spec.description)
self.assertNotEqual("# This is some documentation I wanted to add.",
processor2.bpmn_workflow.last_task.task_spec.documentation)
# You can do a soft update and get the right response.
processor3 = WorkflowProcessor(processor.workflow_model, soft_reset=True)
self.assertEqual("Step 1", processor3.bpmn_workflow.last_task.task_spec.description)
self.assertEqual("# This is some documentation I wanted to add.",
processor3.bpmn_workflow.last_task.task_spec.documentation)
def test_hard_reset(self):
self.load_example_data()
@ -344,8 +297,10 @@ class TestWorkflowProcessor(BaseTest):
# Do a hard reset, which should bring us back to the beginning, but retain the data.
processor3 = WorkflowProcessor(processor.workflow_model, hard_reset=True)
self.assertEqual("Step 1", processor3.next_task().task_spec.description)
self.assertEqual({"color": "blue"}, processor3.next_task().data)
processor3.complete_task(processor3.next_task())
self.assertTrue(processor3.is_latest_spec) # Now at version 2.
task = processor3.next_task()
task.data = {"color": "blue"}
processor3.complete_task(task)
self.assertEqual("New Step", processor3.next_task().task_spec.description)
self.assertEqual("blue", processor3.next_task().data["color"])
@ -413,4 +368,19 @@ class TestWorkflowProcessor(BaseTest):
task.task_spec.form.fields.append(field)
with self.assertRaises(ApiError):
self._populate_form_with_random_data(task)
self._populate_form_with_random_data(task)
def test_get_role_by_name(self):
self.load_example_data()
workflow_spec_model = self.load_test_spec("roles")
study = session.query(StudyModel).first()
processor = self.get_processor(study, workflow_spec_model)
processor.do_engine_steps()
tasks = processor.next_user_tasks()
task = tasks[0]
self._populate_form_with_random_data(task)
processor.complete_task(task)
supervisor_task = processor.next_user_tasks()[0]
self.assertEquals("supervisor", supervisor_task.task_spec.lane)

View File

@ -1,13 +1,13 @@
from unittest.mock import patch
from tests.base_test import BaseTest
from crc import session
from crc import session, db
from crc.models.api_models import MultiInstanceType
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowStatus
from crc.models.workflow import WorkflowStatus, WorkflowModel
from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
class TestWorkflowProcessorMultiInstance(BaseTest):
@ -32,7 +32,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
'error': 'Unable to locate a user with id asd3v in LDAP'}}
def _populate_form_with_random_data(self, task):
WorkflowProcessor.populate_form_with_random_data(task)
WorkflowService.populate_form_with_random_data(task)
def get_processor(self, study_model, spec_model):
workflow_model = StudyService._create_workflow_model(study_model, spec_model)
@ -51,51 +51,72 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
self.assertIsNotNone(processor)
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
processor.bpmn_workflow.do_engine_steps()
next_user_tasks = processor.next_user_tasks()
self.assertEqual(1, len(next_user_tasks))
task = next_user_tasks[0]
workflow_api = WorkflowService.processor_to_workflow_api(processor)
self.assertIsNotNone(workflow_api)
self.assertIsNotNone(workflow_api.next_task)
# 1st investigator
api_task = workflow_api.next_task
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
self.assertEqual("dhf8r", task.data["investigator"]["user_id"])
self.assertEqual("MutiInstanceTask", task.get_name())
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual(MultiInstanceType.sequential, api_task.multi_instance_type)
self.assertEqual("dhf8r", api_task.data["investigator"]["user_id"])
self.assertTrue(api_task.name.startswith("MultiInstanceTask"))
self.assertEqual(3, api_task.multi_instance_count)
self.assertEqual(1, api_task.multi_instance_index)
task.update_data({"investigator":{"email":"asd3v@virginia.edu"}})
task = processor.get_current_user_tasks()[0]
self.assertEqual(task.id, api_task.id)
task.update_data({"investigator": {"email": "asd3v@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
workflow_api = WorkflowService.processor_to_workflow_api(processor)
task = next_user_tasks[0]
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual("MutiInstanceTask", api_task.name)
task.update_data({"investigator":{"email":"asdf32@virginia.edu"}})
# 2nd investigator
api_task = workflow_api.next_task
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
self.assertEqual(None, api_task.data["investigator"]["user_id"])
self.assertTrue(api_task.name.startswith("MultiInstanceTask"))
self.assertEqual(3, api_task.multi_instance_count)
self.assertEqual(2, api_task.multi_instance_index)
task = processor.get_current_user_tasks()[0]
self.assertEqual(task.id, api_task.id)
task.update_data({"investigator": {"email": "asdf32@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
workflow_api = WorkflowService.processor_to_workflow_api(processor)
task = next_user_tasks[0]
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual("MutiInstanceTask", task.get_name())
task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}})
# 3rd investigator
api_task = workflow_api.next_task
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
self.assertEqual("asd3v", api_task.data["investigator"]["user_id"])
self.assertTrue(api_task.name.startswith("MultiInstanceTask"))
self.assertEqual(3, api_task.multi_instance_count)
self.assertEqual(3, api_task.multi_instance_index)
task = processor.get_current_user_tasks()[0]
self.assertEqual(task.id, api_task.id)
task.update_data({"investigator": {"email": "dhf8r@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
task = processor.bpmn_workflow.last_task
workflow_api = WorkflowService.processor_to_workflow_api(processor)
# Last task
api_task = workflow_api.next_task
expected = self.mock_investigator_response
expected['PI']['email'] = "asd3v@virginia.edu"
expected['SC_I']['email'] = "asdf32@virginia.edu"
expected['DC']['email'] = "dhf8r@virginia.edu"
self.assertEqual(expected,
task.data['StudyInfo']['investigators'])
self.assertEqual(expected, api_task.data['StudyInfo']['investigators'])
self.assertEqual(WorkflowStatus.complete, processor.get_status())
def refresh_processor(self, processor):
"""Saves the processor, and returns a new one read in from the database"""
processor.save()
processor = WorkflowProcessor(processor.workflow_model)
return processor
@patch('crc.services.study_service.StudyService.get_investigators')
def test_create_and_complete_workflow_parallel(self, mock_study_service):
"""Unlike the test above, the parallel task allows us to complete the items in any order."""
@ -107,11 +128,15 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
workflow_spec_model = self.load_test_spec("multi_instance_parallel")
study = session.query(StudyModel).first()
processor = self.get_processor(study, workflow_spec_model)
processor = self.refresh_processor(processor)
processor.bpmn_workflow.do_engine_steps()
# In the Parallel instance, there should be three tasks, all of them in the ready state.
next_user_tasks = processor.next_user_tasks()
self.assertEqual(3, len(next_user_tasks))
# There should be six tasks in the navigation: start event, the script task, end event, and three tasks
# for the three executions of hte multi-instance.
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
# We can complete the tasks out of order.
task = next_user_tasks[2]
@ -121,23 +146,31 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual(MultiInstanceType.parallel, api_task.multi_instance_type)
task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}})
# Assure navigation picks up the label of the current element variable.
nav = WorkflowService.processor_to_workflow_api(processor, task).navigation
self.assertEquals("Primary Investigator", nav[2].title)
task.update_data({"investigator": {"email": "dhf8r@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
task = next_user_tasks[0]
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual("MutiInstanceTask", api_task.name)
self.assertEqual("MultiInstanceTask", api_task.name)
task.update_data({"investigator":{"email":"asd3v@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
task = next_user_tasks[1]
api_task = WorkflowService.spiff_task_to_api_task(task)
self.assertEqual("MutiInstanceTask", task.get_name())
self.assertEqual("MultiInstanceTask", task.get_name())
task.update_data({"investigator":{"email":"asdf32@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
# Completing the tasks out of order, still provides the correct information.
expected = self.mock_investigator_response
@ -148,3 +181,4 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task.data['StudyInfo']['investigators'])
self.assertEqual(WorkflowStatus.complete, processor.get_status())
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))

View File

@ -1,7 +1,16 @@
import json
import unittest
from tests.base_test import BaseTest
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
from SpiffWorkflow import Task as SpiffTask, WorkflowException
from example_data import ExampleDataLoader
from crc import db
from crc.models.task_event import TaskEventModel
from crc.models.api_models import Task
from crc.api.common import ApiError
class TestWorkflowService(BaseTest):
@ -78,4 +87,9 @@ class TestWorkflowService(BaseTest):
task = processor.next_task()
task_api = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True)
WorkflowService.populate_form_with_random_data(task, task_api, required_only=False)
self.assertTrue(isinstance(task.data["sponsor"], dict))
self.assertTrue(isinstance(task.data["sponsor"], dict))
def test_dmn_evaluation_errors_in_oncomplete_raise_api_errors_during_validation(self):
workflow_spec_model = self.load_test_spec("decision_table_invalid")
with self.assertRaises(ApiError):
WorkflowService.test_spec(workflow_spec_model.id)

View File

@ -1,4 +1,5 @@
import json
import unittest
from unittest.mock import patch
from tests.base_test import BaseTest
@ -51,9 +52,6 @@ class TestWorkflowSpecValidation(BaseTest):
app.config['PB_ENABLED'] = True
self.validate_all_loaded_workflows()
def test_successful_validation_of_rrt_workflows(self):
self.load_example_data(use_rrt_data=True)
self.validate_all_loaded_workflows()
def validate_all_loaded_workflows(self):
workflows = session.query(WorkflowSpecModel).all()
@ -66,7 +64,6 @@ class TestWorkflowSpecValidation(BaseTest):
errors.extend(ApiErrorSchema(many=True).load(json_data))
self.assertEqual(0, len(errors), json.dumps(errors))
def test_invalid_expression(self):
self.load_example_data()
errors = self.validate_workflow("invalid_expression")
@ -92,12 +89,21 @@ class TestWorkflowSpecValidation(BaseTest):
self.load_example_data()
errors = self.validate_workflow("invalid_script")
self.assertEqual(2, len(errors))
self.assertEqual("workflow_validation_exception", errors[0]['code'])
self.assertEqual("error_loading_workflow", errors[0]['code'])
self.assertTrue("NoSuchScript" in errors[0]['message'])
self.assertEqual("Invalid_Script_Task", errors[0]['task_id'])
self.assertEqual("An Invalid Script Reference", errors[0]['task_name'])
self.assertEqual("invalid_script.bpmn", errors[0]['file_name'])
def test_invalid_script2(self):
self.load_example_data()
errors = self.validate_workflow("invalid_script2")
self.assertEqual(2, len(errors))
self.assertEqual("error_loading_workflow", errors[0]['code'])
self.assertEqual("Invalid_Script_Task", errors[0]['task_id'])
self.assertEqual("An Invalid Script Reference", errors[0]['task_name'])
self.assertEqual("invalid_script2.bpmn", errors[0]['file_name'])
def test_repeating_sections_correctly_populated(self):
self.load_example_data()
spec_model = self.load_test_spec('repeat_form')