Merge pull request #209 from sartography/dev

Email Script, Sentry User details, Workflow Spec Transfers, Navigation refactor
This commit is contained in:
Dan Funk 2020-12-14 21:56:38 -05:00 committed by GitHub
commit cdd3c13dff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2119 additions and 512 deletions

133
Pipfile.lock generated
View File

@ -33,10 +33,10 @@
},
"aniso8601": {
"hashes": [
"sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072",
"sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a"
"sha256:246bf8d3611527030889e6df970878969d3a2f760ba3eb694fa1fb10e6ce53f9",
"sha256:51047d4fb51d7b8afd522b70f2d21a1b2487cbb7f7bd84ea852e9aa7808e7704"
],
"version": "==8.0.0"
"version": "==8.1.0"
},
"attrs": {
"hashes": [
@ -80,10 +80,10 @@
},
"certifi": {
"hashes": [
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.11.8"
"version": "==2020.12.5"
},
"cffi": {
"hashes": [
@ -108,12 +108,14 @@
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
@ -548,40 +550,40 @@
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
],
"version": "==20.4"
"version": "==20.8"
},
"pandas": {
"hashes": [
"sha256:09e0503758ad61afe81c9069505f8cb8c1e36ea8cc1e6826a95823ef5b327daf",
"sha256:0a11a6290ef3667575cbd4785a1b62d658c25a2fd70a5adedba32e156a8f1773",
"sha256:0d9a38a59242a2f6298fff45d09768b78b6eb0c52af5919ea9e45965d7ba56d9",
"sha256:112c5ba0f9ea0f60b2cc38c25f87ca1d5ca10f71efbee8e0f1bee9cf584ed5d5",
"sha256:185cf8c8f38b169dbf7001e1a88c511f653fbb9dfa3e048f5e19c38049e991dc",
"sha256:3aa8e10768c730cc1b610aca688f588831fa70b65a26cb549fbb9f35049a05e0",
"sha256:41746d520f2b50409dffdba29a15c42caa7babae15616bcf80800d8cfcae3d3e",
"sha256:43cea38cbcadb900829858884f49745eb1f42f92609d368cabcc674b03e90efc",
"sha256:5378f58172bd63d8c16dd5d008d7dcdd55bf803fcdbe7da2dcb65dbbf322f05b",
"sha256:54404abb1cd3f89d01f1fb5350607815326790efb4789be60508f458cdd5ccbf",
"sha256:5dac3aeaac5feb1016e94bde851eb2012d1733a222b8afa788202b836c97dad5",
"sha256:5fdb2a61e477ce58d3f1fdf2470ee142d9f0dde4969032edaf0b8f1a9dafeaa2",
"sha256:6613c7815ee0b20222178ad32ec144061cb07e6a746970c9160af1ebe3ad43b4",
"sha256:6d2b5b58e7df46b2c010ec78d7fb9ab20abf1d306d0614d3432e7478993fbdb0",
"sha256:8a5d7e57b9df2c0a9a202840b2881bb1f7a648eba12dd2d919ac07a33a36a97f",
"sha256:8b4c2055ebd6e497e5ecc06efa5b8aa76f59d15233356eb10dad22a03b757805",
"sha256:a15653480e5b92ee376f8458197a58cca89a6e95d12cccb4c2d933df5cecc63f",
"sha256:a7d2547b601ecc9a53fd41561de49a43d2231728ad65c7713d6b616cd02ddbed",
"sha256:a979d0404b135c63954dea79e6246c45dd45371a88631cdbb4877d844e6de3b6",
"sha256:b1f8111635700de7ac350b639e7e452b06fc541a328cf6193cf8fc638804bab8",
"sha256:c5a3597880a7a29a31ebd39b73b2c824316ae63a05c3c8a5ce2aea3fc68afe35",
"sha256:c681e8fcc47a767bf868341d8f0d76923733cbdcabd6ec3a3560695c69f14a1e",
"sha256:cf135a08f306ebbcfea6da8bf775217613917be23e5074c69215b91e180caab4",
"sha256:e2b8557fe6d0a18db4d61c028c6af61bfed44ef90e419ed6fadbdc079eba141e"
"sha256:0a643bae4283a37732ddfcecab3f62dd082996021b980f580903f4e8e01b3c5b",
"sha256:0de3ddb414d30798cbf56e642d82cac30a80223ad6fe484d66c0ce01a84d6f2f",
"sha256:19a2148a1d02791352e9fa637899a78e371a3516ac6da5c4edc718f60cbae648",
"sha256:21b5a2b033380adbdd36b3116faaf9a4663e375325831dac1b519a44f9e439bb",
"sha256:24c7f8d4aee71bfa6401faeba367dd654f696a77151a8a28bc2013f7ced4af98",
"sha256:26fa92d3ac743a149a31b21d6f4337b0594b6302ea5575b37af9ca9611e8981a",
"sha256:2860a97cbb25444ffc0088b457da0a79dc79f9c601238a3e0644312fcc14bf11",
"sha256:2b1c6cd28a0dfda75c7b5957363333f01d370936e4c6276b7b8e696dd500582a",
"sha256:2c2f7c670ea4e60318e4b7e474d56447cf0c7d83b3c2a5405a0dbb2600b9c48e",
"sha256:3be7a7a0ca71a2640e81d9276f526bca63505850add10206d0da2e8a0a325dae",
"sha256:4c62e94d5d49db116bef1bd5c2486723a292d79409fc9abd51adf9e05329101d",
"sha256:5008374ebb990dad9ed48b0f5d0038124c73748f5384cc8c46904dace27082d9",
"sha256:5447ea7af4005b0daf695a316a423b96374c9c73ffbd4533209c5ddc369e644b",
"sha256:573fba5b05bf2c69271a32e52399c8de599e4a15ab7cec47d3b9c904125ab788",
"sha256:5a780260afc88268a9d3ac3511d8f494fdcf637eece62fb9eb656a63d53eb7ca",
"sha256:70865f96bb38fec46f7ebd66d4b5cfd0aa6b842073f298d621385ae3898d28b5",
"sha256:731568be71fba1e13cae212c362f3d2ca8932e83cb1b85e3f1b4dd77d019254a",
"sha256:b61080750d19a0122469ab59b087380721d6b72a4e7d962e4d7e63e0c4504814",
"sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d",
"sha256:c16d59c15d946111d2716856dd5479221c9e4f2f5c7bc2d617f39d870031e086",
"sha256:c61c043aafb69329d0f961b19faa30b1dab709dd34c9388143fc55680059e55a",
"sha256:c94ff2780a1fd89f190390130d6d36173ca59fcfb3fe0ff596f9a56518191ccb",
"sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782",
"sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b"
],
"index": "pypi",
"version": "==1.1.4"
"version": "==1.1.5"
},
"psycopg2-binary": {
"hashes": [
@ -640,18 +642,18 @@
},
"pygithub": {
"hashes": [
"sha256:776befaddab9d8fddd525d52a6ca1ac228cf62b5b1e271836d766f4925e1452e",
"sha256:8ad656bf79958e775ec59f7f5a3dbcbadac12147ae3dc42708b951064096af15"
"sha256:053f1b8d553a344ebd3ca3972765d923ee7e8ecc3ea55bd203683f164348fa1a",
"sha256:14c96d55e3c0e295598e52fbbbf2a7862a293723482ae9000cb9c816faab4fb4"
],
"index": "pypi",
"version": "==1.53"
"version": "==1.54"
},
"pygments": {
"hashes": [
"sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0",
"sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"
"sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716",
"sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"
],
"version": "==2.7.2"
"version": "==2.7.3"
},
"pyjwt": {
"hashes": [
@ -746,11 +748,11 @@
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"index": "pypi",
"version": "==2.25.0"
"version": "==2.24.0"
},
"sentry-sdk": {
"extras": [
@ -779,11 +781,11 @@
},
"soupsieve": {
"hashes": [
"sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55",
"sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"
"sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851",
"sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e"
],
"markers": "python_version >= '3.0'",
"version": "==2.0.1"
"version": "==2.1"
},
"sphinx": {
"hashes": [
@ -837,7 +839,7 @@
},
"spiffworkflow": {
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "6b2ed24bb340ebd31049312bd321f66ebf7b6b26"
"ref": "450c07b886ce6bd8425974f0349248daade90fa0"
},
"sqlalchemy": {
"hashes": [
@ -892,10 +894,10 @@
},
"urllib3": {
"hashes": [
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
],
"version": "==1.26.2"
"version": "==1.25.11"
},
"waitress": {
"hashes": [
@ -942,11 +944,11 @@
},
"xlrd": {
"hashes": [
"sha256:546eb36cee8db40c3eaa46c351e67ffee6eeb5fa2650b71bc4c758a29a1b29b2",
"sha256:e551fb498759fa3a5384a94ccd4c3c02eb7c00ea424426e212ac0c57be9dfbde"
"sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd",
"sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"
],
"index": "pypi",
"version": "==1.2.0"
"version": "==2.0.1"
},
"xlsxwriter": {
"hashes": [
@ -1014,10 +1016,10 @@
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
],
"version": "==20.4"
"version": "==20.8"
},
"pbr": {
"hashes": [
@ -1036,10 +1038,10 @@
},
"py": {
"hashes": [
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"version": "==1.9.0"
"version": "==1.10.0"
},
"pyparsing": {
"hashes": [
@ -1050,18 +1052,11 @@
},
"pytest": {
"hashes": [
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
"sha256:b12e09409c5bdedc28d308469e156127004a436b41e9b44f9bff6446cbab9152",
"sha256:d69e1a80b34fe4d596c9142f35d9e523d98a2838976f1a68419a8f051b24cec6"
],
"index": "pypi",
"version": "==6.1.2"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
"version": "==6.2.0"
},
"toml": {
"hashes": [

View File

@ -6,6 +6,14 @@ basedir = os.path.abspath(os.path.dirname(__file__))
JSON_SORT_KEYS = False # CRITICAL. Do not sort the data when returning values to the front end.
# The API_TOKEN is used to ensure that the
# workflow synch can work without a lot of
# back and forth.
# you may want to change this to something simple for testing!!
# NB, if you change this in the local endpoint,
# it needs to be changed in the remote endpoint as well
API_TOKEN = environ.get('API_TOKEN', default = 'af95596f327c9ecc007b60414fc84b61')
NAME = "CR Connect Workflow"
FLASK_PORT = environ.get('PORT0') or environ.get('FLASK_PORT', default="5000")
CORS_ALLOW_ORIGINS = re.split(r',\s*', environ.get('CORS_ALLOW_ORIGINS', default="localhost:4200, localhost:5002"))

View File

@ -100,6 +100,200 @@ paths:
type: array
items:
$ref: "#/components/schemas/Study"
/workflow_sync/pullall:
get:
operationId: crc.api.workflow_sync.sync_all_changed_workflows
summary: Sync all workflows that have changed on the remote side and provide a list of the results
security:
- ApiKeyAuth : []
# in the endpoint
parameters:
- name: remote
in: query
required: true
description: The remote endpoint
schema:
type: string
tags:
- Workflow Sync API
responses:
'200':
description: An array of workflow specs that were synced from remote.
content:
application/json:
schema:
type: array
items:
type: string
example : ['top_level_workflow','3b495037-f7d4-4509-bf58-cee41c0c6b0e']
/workflow_sync/diff:
get:
operationId: crc.api.workflow_sync.get_changed_workflows
summary: Provides a list of workflow that differ from remote and if it is new or not
security :
- ApiKeyAuth : []
# in the endpoint
parameters:
- name: remote
in: query
required: true
description: The remote endpoint
schema:
type: string
tags:
- Workflow Sync API
responses:
'200':
description: An array of workflow specs, with last touched date and which one is most recent.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WorkflowSpecDiffList"
/workflow_sync/{workflow_spec_id}/spec:
parameters:
- name: workflow_spec_id
in: path
required: false
description: The unique id of an existing workflow specification to modify.
schema:
type: string
get:
operationId: crc.api.workflow_sync.get_sync_workflow_specification
summary: Returns a single workflow specification
security:
- ApiKeyAuth: []
tags:
- Workflow Sync API
responses:
'200':
description: Workflow specification.
content:
application/json:
schema:
$ref: "#/components/schemas/WorkflowSpec"
/workflow_sync/{workflow_spec_id}/files:
get:
operationId: crc.api.workflow_sync.get_workflow_spec_files
summary: Provides a list of files for a workflow spec on this machine.
security :
- ApiKeyAuth : []
parameters:
- name: workflow_spec_id
in: path
required: true
description: The workflow_spec id
schema:
type: string
tags:
- Workflow Sync API
responses:
'200':
description: An array of files for a workflow spec on the local system, with details.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WorkflowSpecFilesList"
/workflow_sync/{workflow_spec_id}/files/sync:
get:
operationId: crc.api.workflow_sync.sync_changed_files
summary: Syncs files from a workflow on a remote system and provides a list of files that were updated
security :
- ApiKeyAuth : []
parameters:
- name: workflow_spec_id
in: path
required: true
description: The workflow_spec id
schema:
type: string
- name: remote
in: query
required: true
description: The remote endpoint
schema:
type: string
tags:
- Workflow Sync API
responses:
'200':
description: A list of files that were synced for the workflow.
content:
application/json:
schema:
type: array
items:
type : string
example : ["data_security_plan.dmn",'some_other_file.xml']
/workflow_sync/{workflow_spec_id}/files/diff:
get:
operationId: crc.api.workflow_sync.get_changed_files
summary: Provides a list of files for a workflow specs that differ from remote and their signature.
security :
- ApiKeyAuth : []
parameters:
- name: workflow_spec_id
in: path
required: true
description: The workflow_spec id
schema:
type: string
- name: remote
in: query
required: true
description: The remote endpoint
schema:
type: string
tags:
- Workflow Sync API
responses:
'200':
description: An array of files that are different from remote, with last touched date and file signature.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WorkflowSpecFilesDiff"
/workflow_sync/all:
get:
operationId: crc.api.workflow_sync.get_all_spec_state
summary: Provides a list of workflow specs, last update date and thumbprint
security:
- ApiKeyAuth : []
tags:
- Workflow Sync API
responses:
'200':
description: An array of workflow specs, with last touched date and file signature.
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WorkflowSpecAll"
/study/all:
get:
operationId: crc.api.study.all_studies
@ -474,6 +668,30 @@ paths:
responses:
'204':
description: The file has been removed.
/file/{md5_hash}/hash_data:
parameters:
- name: md5_hash
in: path
required: true
description: The md5 hash of the file requested
schema:
type: string
get:
operationId: crc.api.file.get_file_data_by_hash
summary: Returns only the file contents
security:
- ApiKeyAuth: []
tags:
- Files
responses:
'200':
description: Returns the actual file
content:
application/octet-stream:
schema:
type: string
format: binary
example: '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>'
/file/{file_id}/data:
parameters:
- name: file_id
@ -1154,6 +1372,12 @@ components:
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: crc.api.user.verify_token_admin
ApiKeyAuth :
type : apiKey
in : header
name : X-CR-API-KEY
x-apikeyInfoFunc: crc.api.workflow_sync.verify_token
schemas:
User:
properties:
@ -1177,6 +1401,92 @@ components:
properties:
id:
type: string
WorkflowSpecDiffList:
properties:
workflow_spec_id:
type: string
example : top_level_workflow
date_created :
type: string
example : 2020-12-09 16:55:12.951500+00:00
location :
type : string
example : remote
new :
type : boolean
example : false
WorkflowSpecFilesList:
properties:
file_model_id:
type : integer
example : 171
workflow_spec_id :
type: string
example : top_level_workflow
filename :
type: string
example : data_security_plan.dmn
date_created :
type: string
example : 2020-12-01 13:58:12.420333+00:00
type:
type : string
example : dmn
primary :
type : boolean
example : false
content_type:
type: string
example : text/xml
primary_process_id:
type : string
example : null
md5_hash:
type: string
example: f12e2bbd-a20c-673b-ccb8-a8a1ea9c5b7b
WorkflowSpecFilesDiff:
properties:
filename :
type: string
example : data_security_plan.dmn
date_created :
type: string
example : 2020-12-01 13:58:12.420333+00:00
type:
type : string
example : dmn
primary :
type : boolean
example : false
content_type:
type: string
example : text/xml
primary_process_id:
type : string
example : null
md5_hash:
type: string
example: f12e2bbd-a20c-673b-ccb8-a8a1ea9c5b7b
location:
type : string
example : remote
new:
type: boolean
example : false
WorkflowSpecAll:
properties:
workflow_spec_id :
type: string
example : acaf1258-43b4-437e-8846-f612afa66811
date_created :
type: string
example : 2020-12-01 13:58:12.420333+00:00
md5_hash:
type: string
example: c30fd597f21715018eab12f97f9d4956
Study:
properties:
id:

View File

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

View File

@ -6,7 +6,7 @@ from flask import send_file
from crc import session
from crc.api.common import ApiError
from crc.models.file import FileSchema, FileModel, File, FileModelSchema
from crc.models.file import FileSchema, FileModel, File, FileModelSchema, FileDataModel
from crc.models.workflow import WorkflowSpecModel
from crc.services.file_service import FileService
@ -99,6 +99,9 @@ def update_file_data(file_id):
file_model = FileService.update_file(file_model, file.stream.read(), file.content_type)
return FileSchema().dump(to_file_api(file_model))
def get_file_data_by_hash(md5_hash):
filedatamodel = session.query(FileDataModel).filter(FileDataModel.md5_hash == md5_hash).first()
return get_file_data(filedatamodel.file_model_id,version=filedatamodel.version)
def get_file_data(file_id, version=None):
file_data = FileService.get_file_data(file_id, version)

View File

@ -1,13 +1,13 @@
import uuid
from SpiffWorkflow.util.deep_merge import DeepMerge
from flask import g
from crc import session, app
from crc import session
from crc.api.common import ApiError, ApiErrorSchema
from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema
from crc.models.api_models import WorkflowApiSchema
from crc.models.file import FileModel, LookupDataSchema
from crc.models.study import StudyModel, WorkflowMetadata
from crc.models.task_event import TaskEventModel, TaskEventModelSchema, TaskEvent, TaskEventSchema
from crc.models.task_event import TaskEventModel, TaskEvent, TaskEventSchema
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \
WorkflowSpecCategoryModelSchema
from crc.services.file_service import FileService

313
crc/api/workflow_sync.py Normal file
View File

@ -0,0 +1,313 @@
import hashlib
import json
import pandas as pd
import requests
from crc import session, app
from crc.api.common import ApiError
from crc.models.file import FileModel, FileDataModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecCategoryModel
from crc.services.file_service import FileService
from crc.services.workflow_sync import WorkflowSyncService
from crc.api.workflow import get_workflow_specification
def get_sync_workflow_specification(workflow_spec_id):
return get_workflow_specification(workflow_spec_id)
def join_uuids(uuids):
"""Joins a pandas Series of uuids and combines them in one hash"""
combined_uuids = ''.join([str(uuid) for uuid in uuids.sort_values()]) # ensure that values are always
# in the same order
return hashlib.md5(combined_uuids.encode('utf8')).hexdigest() # make a hash of the hashes
def verify_token(token, required_scopes):
if token == app.config['API_TOKEN']:
return {'scope':['any']}
else:
raise ApiError("permission_denied", "API Token information is not correct")
def get_changed_workflows(remote,as_df=False):
"""
gets a remote endpoint - gets the workflows and then
determines what workflows are different from the remote endpoint
"""
remote_workflows_list = WorkflowSyncService.get_all_remote_workflows(remote)
remote_workflows = pd.DataFrame(remote_workflows_list)
# get the local thumbprints & make sure that 'workflow_spec_id' is a column, not an index
local = get_all_spec_state_dataframe().reset_index()
# merge these on workflow spec id and hash - this will
# make two different date columns date_x and date_y
different = remote_workflows.merge(local,
right_on=['workflow_spec_id','md5_hash'],
left_on=['workflow_spec_id','md5_hash'],
how = 'outer' ,
indicator=True).loc[lambda x : x['_merge']!='both']
if len(different)==0:
return []
# each line has a tag on it - if was in the left or the right,
# label it so we know if that was on the remote or local machine
different.loc[different['_merge']=='left_only','location'] = 'remote'
different.loc[different['_merge']=='right_only','location'] = 'local'
# this takes the different date_created_x and date-created_y columns and
# combines them back into one date_created column
index = different['date_created_x'].isnull()
different.loc[index,'date_created_x'] = different[index]['date_created_y']
different = different[['workflow_spec_id','date_created_x','location']].copy()
different.columns=['workflow_spec_id','date_created','location']
# our different list will have multiple entries for a workflow if there is a version on either side
# we want to grab the most recent one, so we sort and grab the most recent one for each workflow
changedfiles = different.sort_values('date_created',ascending=False).groupby('workflow_spec_id').first()
# get an exclusive or list of workflow ids - that is we want lists of files that are
# on one machine or the other, but not both
remote_spec_ids = remote_workflows[['workflow_spec_id']]
local_spec_ids = local[['workflow_spec_id']]
left = remote_spec_ids[~remote_spec_ids['workflow_spec_id'].isin(local_spec_ids['workflow_spec_id'])]
right = local_spec_ids[~local_spec_ids['workflow_spec_id'].isin(remote_spec_ids['workflow_spec_id'])]
# flag files as new that are only on the remote box and remove the files that are only on the local box
changedfiles['new'] = False
changedfiles.loc[changedfiles.index.isin(left['workflow_spec_id']), 'new'] = True
output = changedfiles[~changedfiles.index.isin(right['workflow_spec_id'])]
# return the list as a dict, let swagger convert it to json
if as_df:
return output
else:
return output.reset_index().to_dict(orient='records')
def sync_all_changed_workflows(remote):
workflowsdf = get_changed_workflows(remote,as_df=True)
if len(workflowsdf) ==0:
return []
workflows = workflowsdf.reset_index().to_dict(orient='records')
for workflow in workflows:
sync_changed_files(remote,workflow['workflow_spec_id'])
return [x['workflow_spec_id'] for x in workflows]
def sync_changed_files(remote,workflow_spec_id):
# make sure that spec is local before syncing files
specdict = WorkflowSyncService.get_remote_workflow_spec(remote,workflow_spec_id)
localspec = session.query(WorkflowSpecModel).filter(WorkflowSpecModel.id == workflow_spec_id).first()
if localspec is None:
localspec = WorkflowSpecModel()
localspec.id = workflow_spec_id
if specdict['category'] == None:
localspec.category = None
else:
localcategory = session.query(WorkflowSpecCategoryModel).filter(WorkflowSpecCategoryModel.name
== specdict['category']['name']).first()
if localcategory == None:
#category doesn't exist - lets make it
localcategory = WorkflowSpecCategoryModel()
localcategory.name = specdict['category']['name']
localcategory.display_name = specdict['category']['display_name']
localcategory.display_order = specdict['category']['display_order']
session.add(localcategory)
localspec.category = localcategory
localspec.display_order = specdict['display_order']
localspec.display_name = specdict['display_name']
localspec.name = specdict['name']
localspec.description = specdict['description']
session.add(localspec)
changedfiles = get_changed_files(remote,workflow_spec_id,as_df=True)
if len(changedfiles)==0:
return []
updatefiles = changedfiles[~((changedfiles['new']==True) & (changedfiles['location']=='local'))]
updatefiles = updatefiles.reset_index().to_dict(orient='records')
deletefiles = changedfiles[((changedfiles['new']==True) & (changedfiles['location']=='local'))]
deletefiles = deletefiles.reset_index().to_dict(orient='records')
for delfile in deletefiles:
currentfile = session.query(FileModel).filter(FileModel.workflow_spec_id==workflow_spec_id,
FileModel.name == delfile['filename']).first()
# it is more appropriate to archive the file than delete
# due to the fact that we might have workflows that are using the
# file data
currentfile.archived = True
session.add(currentfile)
for updatefile in updatefiles:
currentfile = session.query(FileModel).filter(FileModel.workflow_spec_id==workflow_spec_id,
FileModel.name == updatefile['filename']).first()
if not currentfile:
currentfile = FileModel()
currentfile.name = updatefile['filename']
currentfile.workflow_spec_id = workflow_spec_id
currentfile.date_created = updatefile['date_created']
currentfile.type = updatefile['type']
currentfile.primary = updatefile['primary']
currentfile.content_type = updatefile['content_type']
currentfile.primary_process_id = updatefile['primary_process_id']
session.add(currentfile)
content = WorkflowSyncService.get_remote_file_by_hash(remote,updatefile['md5_hash'])
FileService.update_file(currentfile,content,updatefile['type'])
session.commit()
return [x['filename'] for x in updatefiles]
def get_changed_files(remote,workflow_spec_id,as_df=False):
"""
gets a remote endpoint - gets the files for a workflow_spec on both
local and remote and determines what files have been change and returns a list of those
files
"""
remote_file_list = WorkflowSyncService.get_remote_workflow_spec_files(remote,workflow_spec_id)
remote_files = pd.DataFrame(remote_file_list)
# get the local thumbprints & make sure that 'workflow_spec_id' is a column, not an index
local = get_workflow_spec_files_dataframe(workflow_spec_id).reset_index()
local['md5_hash'] = local['md5_hash'].astype('str')
remote_files['md5_hash'] = remote_files['md5_hash'].astype('str')
different = remote_files.merge(local,
right_on=['filename','md5_hash'],
left_on=['filename','md5_hash'],
how = 'outer' ,
indicator=True).loc[lambda x : x['_merge']!='both']
if len(different) == 0:
if as_df:
return different
else:
return []
# each line has a tag on it - if was in the left or the right,
# label it so we know if that was on the remote or local machine
different.loc[different['_merge']=='left_only','location'] = 'remote'
different.loc[different['_merge']=='right_only','location'] = 'local'
# this takes the different date_created_x and date-created_y columns and
# combines them back into one date_created column
dualfields = ['date_created','type','primary','content_type','primary_process_id']
for merge in dualfields:
index = different[merge+'_x'].isnull()
different.loc[index,merge+'_x'] = different[index][merge+'_y']
fieldlist = [fld+'_x' for fld in dualfields]
different = different[ fieldlist + ['md5_hash','filename','location']].copy()
different.columns=dualfields+['md5_hash','filename','location']
# our different list will have multiple entries for a workflow if there is a version on either side
# we want to grab the most recent one, so we sort and grab the most recent one for each workflow
changedfiles = different.sort_values('date_created',ascending=False).groupby('filename').first()
# get an exclusive or list of workflow ids - that is we want lists of files that are
# on one machine or the other, but not both
remote_spec_ids = remote_files[['filename']]
local_spec_ids = local[['filename']]
left = remote_spec_ids[~remote_spec_ids['filename'].isin(local_spec_ids['filename'])]
right = local_spec_ids[~local_spec_ids['filename'].isin(remote_spec_ids['filename'])]
changedfiles['new'] = False
changedfiles.loc[changedfiles.index.isin(left['filename']), 'new'] = True
changedfiles.loc[changedfiles.index.isin(right['filename']),'new'] = True
changedfiles = changedfiles.replace({pd.np.nan: None})
# return the list as a dict, let swagger convert it to json
if as_df:
return changedfiles
else:
return changedfiles.reset_index().to_dict(orient='records')
def get_all_spec_state():
"""
Return a list of all workflow specs along with last updated date and a
thumbprint of all of the files that are used for that workflow_spec
Convert into a dict list from a dataframe
"""
df = get_all_spec_state_dataframe()
return df.reset_index().to_dict(orient='records')
def get_workflow_spec_files(workflow_spec_id):
"""
Return a list of all workflow specs along with last updated date and a
thumbprint of all of the files that are used for that workflow_spec
Convert into a dict list from a dataframe
"""
df = get_workflow_spec_files_dataframe(workflow_spec_id)
return df.reset_index().to_dict(orient='records')
def get_workflow_spec_files_dataframe(workflowid):
"""
Return a list of all files for a workflow_spec along with last updated date and a
hash so we can determine file differences for a changed workflow on a box.
Return a dataframe
"""
x = session.query(FileDataModel).join(FileModel).filter(FileModel.workflow_spec_id==workflowid)
# there might be a cleaner way of getting a data frome from some of the
# fields in the ORM - but this works OK
filelist = []
for file in x:
filelist.append({'file_model_id':file.file_model_id,
'workflow_spec_id': file.file_model.workflow_spec_id,
'md5_hash':file.md5_hash,
'filename':file.file_model.name,
'type':file.file_model.type.name,
'primary':file.file_model.primary,
'content_type':file.file_model.content_type,
'primary_process_id':file.file_model.primary_process_id,
'date_created':file.date_created})
if len(filelist) == 0:
return pd.DataFrame(columns=['file_model_id',
'workflow_spec_id',
'md5_hash',
'filename',
'type',
'primary',
'content_type',
'primary_process_id',
'date_created'])
df = pd.DataFrame(filelist).sort_values('date_created').groupby('file_model_id').last()
df['date_created'] = df['date_created'].astype('str')
return df
def get_all_spec_state_dataframe():
"""
Return a list of all workflow specs along with last updated date and a
thumbprint of all of the files that are used for that workflow_spec
Return a dataframe
"""
x = session.query(FileDataModel).join(FileModel)
# there might be a cleaner way of getting a data frome from some of the
# fields in the ORM - but this works OK
filelist = []
for file in x:
filelist.append({'file_model_id':file.file_model_id,
'workflow_spec_id': file.file_model.workflow_spec_id,
'md5_hash':file.md5_hash,
'filename':file.file_model.name,
'date_created':file.date_created})
df = pd.DataFrame(filelist)
# get a distinct list of file_model_id's with the most recent file_data retained
df = df.sort_values('date_created').drop_duplicates(['file_model_id'],keep='last').copy()
# take that list and then group by workflow_spec and retain the most recently touched file
# and make a consolidated hash of the md5_checksums - this acts as a 'thumbprint' for each
# workflow spec
df = df.groupby('workflow_spec_id').agg({'date_created':'max',
'md5_hash':join_uuids}).copy()
# get only the columns we are really interested in returning
df = df[['date_created','md5_hash']].copy()
# convert dates to string
df['date_created'] = df['date_created'].astype('str')
return df

View File

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

View File

@ -69,7 +69,7 @@ class WorkflowStatus(enum.Enum):
class WorkflowSpecDependencyFile(db.Model):
"""Connects a workflow to the version of the specification files it depends on to execute"""
"""Connects to a workflow to test the version of the specification files it depends on to execute"""
file_data_id = db.Column(db.Integer, db.ForeignKey(FileDataModel.id), primary_key=True)
workflow_id = db.Column(db.Integer, db.ForeignKey("workflow.id"), primary_key=True)

View File

@ -1,3 +1,5 @@
import re
import markdown
from jinja2 import Template
@ -24,13 +26,18 @@ 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_email_recipients(task, args)
self.get_content(task)
def do_task(self, task, *args, **kwargs):
args = [arg for arg in args if type(arg) == str]
subject = self.get_subject(task, args)
recipients = self.get_users_info(task, args)
args = [arg for arg in args if type(arg) == str or type(arg) == list]
subject = args[0]
recipients = None
try:
recipients = self.get_email_recipients(task, args)
except ApiError:
raise
content, content_html = self.get_content(task)
if recipients:
send_mail(
@ -41,44 +48,81 @@ Email Subject ApprvlApprvr1 PIComputingID
content_html=content_html
)
def get_users_info(self, task, args):
if len(args) < 1:
raise ApiError(code="missing_argument",
message="Email script requires at least one argument. The "
"name of the variable in the task data that contains user"
"id to process. Multiple arguments are accepted.")
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__)
def check_valid_email(self, email):
# regex from https://emailregex.com/
regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
if (re.search(regex, email)):
return True
else:
return False
def get_email_recipients(self, task, args):
emails = []
if len(args[1]) < 1:
raise ApiError(code="missing_argument",
message="Email script requires at least one email address as an argument. "
"Multiple email addresses are accepted.")
if isinstance(args[1], str):
if self.check_valid_email(args[1]):
emails.append(args[1])
else:
raise ApiError(code="invalid_argument",
message="The email address you provided could not be parsed. "
"The value you provided is '%s" % args[1])
if isinstance(args[1], list):
for address in args[1]:
if self.check_valid_email(address):
emails.append(address)
else:
raise ApiError(code="invalid_argument",
message="The email address you provided could not be parsed. "
"The value you provided is '%s" % address)
if len(emails) > 0:
return emails
else:
raise ApiError(code="invalid_argument",
message="Email script requires a valid email address.")
# 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:
# subject = ''
if len(args[0]) < 1:
raise ApiError(code="missing_argument",
message="Email script requires at least one subject argument. The "
"name of the variable in the task data that contains subject"
" to process. Multiple arguments are accepted.")
message="No subject was provided for the email message.")
subject = args[0]
if not isinstance(subject, str):
if not subject or not isinstance(subject, str):
raise ApiError(code="invalid_argument",
message="The Email script requires 1 argument. The "
"the name of the variable in the task data that contains user"
"ids to process. This must point to an array or a string, but "
"it currently points to a %s " % subject.__class__.__name__)
message="The subject you provided could not be parsed. "
"The value is \"%s\" " % subject)
return subject

View File

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

View File

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

View File

@ -6,7 +6,6 @@ from github import Github, GithubObject, UnknownObjectException
from uuid import UUID
from lxml import etree
import flask
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from pandas import ExcelFile
from sqlalchemy import desc
@ -82,7 +81,7 @@ class FileService(object):
you get '1.0' rather than '1'
fixme: This is stupid stupid slow. Place it in the database and just check if it is up to date."""
data_model = FileService.get_reference_file_data(reference_file_name)
xls = ExcelFile(data_model.data)
xls = ExcelFile(data_model.data, engine='openpyxl')
df = xls.parse(xls.sheet_names[0])
for c in int_columns:
df[c] = df[c].fillna(0)

View File

@ -1,13 +1,12 @@
import copy
import json
import string
import uuid
from datetime import datetime
import random
import string
from datetime import datetime
from typing import List
import jinja2
from SpiffWorkflow import Task as SpiffTask, WorkflowException
from SpiffWorkflow import Task as SpiffTask, WorkflowException, NavItem
from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.MultiInstanceTask import MultiInstanceTask
@ -15,17 +14,16 @@ from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask
from SpiffWorkflow.bpmn.specs.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.specs import CancelTask, StartTask
from SpiffWorkflow.specs import CancelTask, StartTask, MultiChoice
from SpiffWorkflow.util.deep_merge import DeepMerge
from flask import g
from jinja2 import Template
from crc import db, app
from crc.api.common import ApiError
from crc.models.api_models import Task, MultiInstanceType, NavigationItem, NavigationItemSchema, WorkflowApi
from crc.models.api_models import Task, MultiInstanceType, WorkflowApi
from crc.models.file import LookupDataModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.task_event import TaskEventModel
from crc.models.user import UserModel, UserModelSchema
from crc.models.workflow import WorkflowModel, WorkflowStatus, WorkflowSpecModel
from crc.services.file_service import FileService
@ -321,33 +319,9 @@ class WorkflowService(object):
"""Returns an API model representing the state of the current workflow, if requested, and
possible, next_task is set to the current_task."""
nav_dict = processor.bpmn_workflow.get_nav_list()
navigation = processor.bpmn_workflow.get_deep_nav_list()
WorkflowService.update_navigation(navigation, processor)
# Some basic cleanup of the title for the for the navigation.
navigation = []
for nav_item in nav_dict:
spiff_task = processor.bpmn_workflow.get_task(nav_item['task_id'])
if 'description' in nav_item:
nav_item['title'] = nav_item.pop('description')
# fixme: duplicate code from the workflow_service. Should only do this in one place.
if nav_item['title'] is not None and ' ' in nav_item['title']:
nav_item['title'] = nav_item['title'].partition(' ')[2]
else:
nav_item['title'] = ""
if spiff_task:
nav_item['task'] = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
nav_item['title'] = nav_item['task'].title # Prefer the task title.
user_uids = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if not UserService.in_list(user_uids, allow_admin_impersonate=True):
nav_item['state'] = WorkflowService.TASK_STATE_LOCKED
else:
nav_item['task'] = None
navigation.append(NavigationItem(**nav_item))
NavigationItemSchema().dump(nav_item)
spec = db.session.query(WorkflowSpecModel).filter_by(id=processor.workflow_spec_id).first()
workflow_api = WorkflowApi(
@ -376,6 +350,29 @@ class WorkflowService(object):
workflow_api.next_task.state = WorkflowService.TASK_STATE_LOCKED
return workflow_api
@staticmethod
def update_navigation(navigation: List[NavItem], processor: WorkflowProcessor):
# Recursive function to walk down through children, and clean up descriptions, and statuses
for nav_item in navigation:
spiff_task = processor.bpmn_workflow.get_task(nav_item.task_id)
if spiff_task:
# Use existing logic to set the description, and alter the state based on permissions.
api_task = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
nav_item.description = api_task.title
user_uids = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if (isinstance(spiff_task.task_spec, UserTask) or isinstance(spiff_task.task_spec, ManualTask)) \
and not UserService.in_list(user_uids, allow_admin_impersonate=True):
nav_item.state = WorkflowService.TASK_STATE_LOCKED
else:
# Strip off the first word in the description, to meet guidlines for BPMN.
if nav_item.description:
if nav_item.description is not None and ' ' in nav_item.description:
nav_item.description = nav_item.description.partition(' ')[2]
# Recurse here
WorkflowService.update_navigation(nav_item.children, processor)
@staticmethod
def get_previously_submitted_data(workflow_id, spiff_task):
""" If the user has completed this task previously, find the form data for the last submission."""
@ -403,6 +400,7 @@ class WorkflowService(object):
return {}
@staticmethod
def spiff_task_to_api_task(spiff_task, add_docs_and_forms=False):
task_type = spiff_task.task_spec.__class__.__name__

View File

@ -0,0 +1,52 @@
import json
from json import JSONDecodeError
from typing import List, Optional
import requests
from crc import app
from crc.api.common import ApiError
class WorkflowSyncService(object):
@staticmethod
def get_remote_file_by_hash(remote,md5_hash):
url = remote+'/v1.0/file/'+md5_hash+'/hash_data'
return WorkflowSyncService.__make_request(url,return_contents=True)
@staticmethod
def get_remote_workflow_spec_files(remote,workflow_spec_id):
url = remote+'/v1.0/workflow_sync/'+workflow_spec_id+'/files'
return WorkflowSyncService.__make_request(url)
@staticmethod
def get_remote_workflow_spec(remote, workflow_spec_id):
"""
this just gets the details of a workflow spec from the
remote side.
"""
url = remote+'/v1.0/workflow-sync/'+workflow_spec_id+'/spec'
return WorkflowSyncService.__make_request(url)
@staticmethod
def get_all_remote_workflows(remote):
url = remote + '/v1.0/workflow_sync/all'
return WorkflowSyncService.__make_request(url)
@staticmethod
def __make_request(url,return_contents=False):
try:
response = requests.get(url,headers={'X-CR-API-KEY':app.config['API_TOKEN']})
except:
raise ApiError("workflow_sync_error",response.text)
if response.ok and response.text:
if return_contents:
return response.content
else:
return json.loads(response.text)
else:
raise ApiError("workflow_sync_error",
"Received an invalid response from the protocol builder (status %s): %s when calling "
"url '%s'." %
(response.status_code, response.text, url))

View File

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

View File

@ -1,75 +1,76 @@
alabaster==0.7.12
alembic==1.4.2
amqp==2.5.2
alembic==1.4.3
aniso8601==8.0.0
attrs==19.3.0
babel==2.8.0
bcrypt==3.1.7
beautifulsoup4==4.9.1
billiard==3.6.3.0
attrs==20.3.0
babel==2.9.0
bcrypt==3.2.0
beautifulsoup4==4.9.3
blinker==1.4
celery==4.4.2
certifi==2020.4.5.1
cffi==1.14.0
certifi==2020.11.8
cffi==1.14.4
chardet==3.0.4
click==7.1.2
clickclick==1.2.2
clickclick==20.10.2
commonmark==0.9.1
configparser==5.0.0
connexion==2.7.0
coverage==5.1
coverage==5.3
deprecated==1.2.10
docutils==0.16
docxtpl==0.9.2
docxtpl==0.11.2
et-xmlfile==1.0.1
flask==1.1.2
flask-admin==1.5.7
flask-bcrypt==0.7.1
flask-cors==3.0.8
flask-marshmallow==0.12.0
flask-cors==3.0.9
flask-mail==0.9.1
flask-marshmallow==0.14.0
flask-migrate==2.5.3
flask-restful==0.3.8
flask-sqlalchemy==2.4.1
flask-sso==0.4.0
future==0.18.2
httpretty==1.0.2
idna==2.9
flask-sqlalchemy==2.4.4
gunicorn==20.0.4
httpretty==1.0.3
idna==2.10
imagesize==1.2.0
importlib-metadata==1.6.0
inflection==0.4.0
inflection==0.5.1
itsdangerous==1.1.0
jdcal==1.4.1
jinja2==2.11.2
jsonschema==3.2.0
kombu==4.6.8
ldap3==2.7
lxml==4.5.1
mako==1.1.2
ldap3==2.8.1
lxml==4.6.2
mako==1.1.3
markdown==3.3.3
markupsafe==1.1.1
marshmallow==3.6.0
marshmallow==3.9.1
marshmallow-enum==1.5.1
marshmallow-sqlalchemy==0.23.0
numpy==1.18.4
openapi-spec-validator==0.2.8
openpyxl==3.0.3
marshmallow-sqlalchemy==0.24.1
numpy==1.19.4
openapi-spec-validator==0.2.9
openpyxl==3.0.5
packaging==20.4
pandas==1.0.3
psycopg2-binary==2.8.5
pandas==1.1.4
psycopg2-binary==2.8.6
pyasn1==0.4.8
pycparser==2.20
pygments==2.6.1
pygithub==1.53
pygments==2.7.2
pyjwt==1.7.1
pyparsing==2.4.7
pyrsistent==0.16.0
pyrsistent==0.17.3
python-box==5.2.0
python-dateutil==2.8.1
python-docx==0.8.10
python-editor==1.0.4
pytz==2020.1
python-levenshtein==0.12.0
pytz==2020.4
pyyaml==5.3.1
recommonmark==0.6.0
requests==2.23.0
six==1.14.0
requests==2.25.0
sentry-sdk==0.14.4
six==1.15.0
snowballstemmer==2.0.0
soupsieve==2.0.1
sphinx==3.0.3
sphinx==3.3.1
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3
@ -77,14 +78,14 @@ sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.4
spiffworkflow
sqlalchemy==1.3.17
swagger-ui-bundle==0.0.6
urllib3==1.25.9
vine==1.3.0
waitress==1.4.3
sqlalchemy==1.3.20
swagger-ui-bundle==0.0.8
urllib3==1.26.2
waitress==1.4.4
webob==1.8.6
webtest==2.0.35
werkzeug==1.0.1
wrapt==1.12.1
wtforms==2.3.3
xlrd==1.2.0
xlsxwriter==1.2.8
zipp==3.1.0
xlsxwriter==1.3.7

View File

@ -66,6 +66,7 @@ class ExampleDataLoader:
display_order=6
),
]
db.session.execute("select setval('workflow_spec_category_id_seq',7);")
db.session.add_all(categories)
db.session.commit()

View File

@ -204,6 +204,14 @@ class BaseTest(unittest.TestCase):
data = myfile.read()
return data
@staticmethod
def workflow_sync_response(file_name):
filepath = os.path.join(app.root_path, '..', 'tests', 'data', 'workflow_sync_responses', file_name)
with open(filepath, 'rb') as myfile:
data = myfile.read()
return data
def assert_success(self, rv, msg=""):
try:
data = json.loads(rv.get_data(as_text=True))

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0y2dq4f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0y2dq4f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_0tad5ma" name="Set Recipients" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1synsig</bpmn:outgoing>
@ -20,7 +20,7 @@ Email content to be delivered to {{ ApprvlApprvr1 }}
---</bpmn:documentation>
<bpmn:incoming>Flow_08n2npe</bpmn:incoming>
<bpmn:outgoing>Flow_1xlrgne</bpmn:outgoing>
<bpmn:script>email("Camunda Email Subject",'ApprvlApprvr1','PIComputingID')</bpmn:script>
<bpmn:script>email("Camunda Email Subject",ApprvlApprvr1,PIComputingID)</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1synsig" sourceRef="StartEvent_1" targetRef="Activity_1l9vih3" />
<bpmn:sequenceFlow id="Flow_1xlrgne" sourceRef="Activity_0s5v97n" targetRef="Event_0izrcj4" />

View File

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

View File

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

View File

@ -0,0 +1,200 @@
<?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.0">
<bpmn:process id="Process_SecondFact" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0c7wlth</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Task_User_Select_Type" name="Set Type" camunda:formKey="Get A Random Fun Fact">
<bpmn:documentation># h1 Heading 8-)
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
## Horizontal Rules
___
---
***
## Typographic replacements
"double quotes" and 'single quotes'
## Emphasis
**This is bold text**
__This is bold text__
*This is italic text*
_This is italic text_
~~Strikethrough~~
## Blockquotes
&gt; Blockquotes can also be nested...
&gt;&gt; ...by using additional greater-than signs right next to each other...
&gt; &gt; &gt; ...or with spaces between arrows.
## Lists
Unordered
+ Create a list by starting a line with `+`, `-`, or `*`
+ Sub-lists are made by indenting 2 spaces:
- Marker character change forces new list start:
* Ac tristique libero volutpat at
+ Facilisis in pretium nisl aliquet
- Nulla volutpat aliquam velit
+ Very easy!
Ordered
1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit
3. Integer molestie lorem at massa
1. You can use sequential numbers...
1. ...or keep all the numbers as `1.`
Start numbering with offset:
57. foo
1. bar
## Tables
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
Right aligned columns
| Option | Description |
| ------:| -----------:|
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
## Links
[link text](http://dev.nodeca.com)
[link with title](http://nodeca.github.io/pica/demo/ "title text!")
Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
## Images
![Minion](https://octodex.github.com/images/minion.png)
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="type" label="Type" type="enum" defaultValue="cat">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="norris" name="Chuck Norris" />
<camunda:value id="cat" name="Cat Fact" />
<camunda:value id="buzzword" name="Business Buzzword" />
</camunda:formField>
</camunda:formData>
<camunda:properties>
<camunda:property name="type" value="string" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0c7wlth</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0641sh6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Task_Get_Fact_From_API" name="Display Fact">
<bpmn:documentation />
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="Fact.type" />
</camunda:inputOutput>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0641sh6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0t29gjo</bpmn:outgoing>
<bpmn:script>FactService = fact_service()</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0u1cgrf">
<bpmn:documentation># Great Job!
You have completed the random fact generator.
You chose to receive a random fact of the type: "{{type}}"
Your random fact is:
{{details}}</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0t29gjo</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0c7wlth" sourceRef="StartEvent_1" targetRef="Task_User_Select_Type" />
<bpmn:sequenceFlow id="SequenceFlow_0641sh6" sourceRef="Task_User_Select_Type" targetRef="Task_Get_Fact_From_API" />
<bpmn:sequenceFlow id="SequenceFlow_0t29gjo" sourceRef="Task_Get_Fact_From_API" targetRef="EndEvent_0u1cgrf" />
<bpmn:textAnnotation id="TextAnnotation_09fq7kh">
<bpmn:text>User sets the Fact.type to cat, norris, or buzzword</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1cfasjp" sourceRef="Task_User_Select_Type" targetRef="TextAnnotation_09fq7kh" />
<bpmn:textAnnotation id="TextAnnotation_1234e5n">
<bpmn:text>Makes an API  call to get a fact of the required type.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1qirnyy" sourceRef="Task_Get_Fact_From_API" targetRef="TextAnnotation_1234e5n" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1ds61df">
<bpmndi:BPMNEdge id="SequenceFlow_0t29gjo_di" bpmnElement="SequenceFlow_0t29gjo">
<di:waypoint x="570" y="250" />
<di:waypoint x="692" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0641sh6_di" bpmnElement="SequenceFlow_0641sh6">
<di:waypoint x="370" y="250" />
<di:waypoint x="470" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0c7wlth_di" bpmnElement="SequenceFlow_0c7wlth">
<di:waypoint x="188" y="250" />
<di:waypoint x="270" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<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="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
<dc:Bounds x="692" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_09fq7kh_di" bpmnElement="TextAnnotation_09fq7kh">
<dc:Bounds x="330" y="116" width="99.99202297383536" height="68.28334396936822" />
</bpmndi:BPMNShape>
<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" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1qirnyy_di" bpmnElement="Association_1qirnyy">
<di:waypoint x="561" y="210" />
<di:waypoint x="584" y="188" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,104 @@
<?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.0">
<bpmn:process id="Process_SecondFact" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0c7wlth</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Task_User_Select_Type" name="Set Type" camunda:formKey="Get A Random Fun Fact">
<bpmn:documentation># h1 Heading 8-)
NEW_FILE_ADDED
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="type" label="Type" type="enum" defaultValue="cat">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="norris" name="Chuck Norris" />
<camunda:value id="cat" name="Cat Fact" />
<camunda:value id="buzzword" name="Business Buzzword" />
</camunda:formField>
</camunda:formData>
<camunda:properties>
<camunda:property name="type" value="string" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0c7wlth</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0641sh6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Task_Get_Fact_From_API" name="Display Fact">
<bpmn:documentation />
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="Fact.type" />
</camunda:inputOutput>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0641sh6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0t29gjo</bpmn:outgoing>
<bpmn:script>FactService = fact_service()</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0u1cgrf">
<bpmn:documentation># Great Job!
You have completed the random fact generator.
You chose to receive a random fact of the type: "{{type}}"
Your random fact is:
{{details}}</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0t29gjo</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0c7wlth" sourceRef="StartEvent_1" targetRef="Task_User_Select_Type" />
<bpmn:sequenceFlow id="SequenceFlow_0641sh6" sourceRef="Task_User_Select_Type" targetRef="Task_Get_Fact_From_API" />
<bpmn:sequenceFlow id="SequenceFlow_0t29gjo" sourceRef="Task_Get_Fact_From_API" targetRef="EndEvent_0u1cgrf" />
<bpmn:textAnnotation id="TextAnnotation_09fq7kh">
<bpmn:text>User sets the Fact.type to cat, norris, or buzzword</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1cfasjp" sourceRef="Task_User_Select_Type" targetRef="TextAnnotation_09fq7kh" />
<bpmn:textAnnotation id="TextAnnotation_1234e5n">
<bpmn:text>Makes an API  call to get a fact of the required type.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1qirnyy" sourceRef="Task_Get_Fact_From_API" targetRef="TextAnnotation_1234e5n" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1ds61df">
<bpmndi:BPMNEdge id="SequenceFlow_0t29gjo_di" bpmnElement="SequenceFlow_0t29gjo">
<di:waypoint x="570" y="250" />
<di:waypoint x="692" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0641sh6_di" bpmnElement="SequenceFlow_0641sh6">
<di:waypoint x="370" y="250" />
<di:waypoint x="470" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0c7wlth_di" bpmnElement="SequenceFlow_0c7wlth">
<di:waypoint x="188" y="250" />
<di:waypoint x="270" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<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="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
<dc:Bounds x="692" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_09fq7kh_di" bpmnElement="TextAnnotation_09fq7kh">
<dc:Bounds x="330" y="116" width="99.99202297383536" height="68.28334396936822" />
</bpmndi:BPMNShape>
<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" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1qirnyy_di" bpmnElement="Association_1qirnyy">
<di:waypoint x="561" y="210" />
<di:waypoint x="584" y="188" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

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

View File

@ -46,7 +46,7 @@ class TestFilesApi(BaseTest):
content_type="application/json", headers=self.logged_in_headers())
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
self.assertEqual(2, len(json_data))
self.assertEqual(3, len(json_data))
def test_create_file(self):

View File

@ -165,7 +165,7 @@ class TestStudyApi(BaseTest):
self.assertEqual(study_event.comment, update_comment)
self.assertEqual(study_event.user_uid, self.test_uid)
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_investigators
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies

View File

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

View File

@ -68,7 +68,7 @@ class TestTasksApi(BaseTest):
# get the first form in the two form workflow.
workflow_api = self.get_workflow_api(workflow)
self.assertEqual('two_forms', workflow_api.workflow_spec_id)
self.assertEqual(2, len(workflow_api.navigation))
self.assertEqual(5, len(workflow_api.navigation))
self.assertIsNotNone(workflow_api.next_task.form)
self.assertEqual("UserTask", workflow_api.next_task.type)
self.assertEqual("StepOne", workflow_api.next_task.name)
@ -113,14 +113,20 @@ class TestTasksApi(BaseTest):
self.assertIsNotNone(workflow_api.navigation)
nav = workflow_api.navigation
self.assertEqual(5, len(nav))
self.assertEqual("Do You Have Bananas", nav[0]['title'])
self.assertEqual("Bananas?", nav[1]['title'])
self.assertEqual("FUTURE", nav[1]['state'])
self.assertEqual("yes", nav[2]['title'])
self.assertEqual("NOOP", nav[2]['state'])
self.assertEqual("no", nav[3]['title'])
self.assertEqual("NOOP", nav[3]['state'])
self.assertEqual(4, len(nav))
self.assertEqual("Do You Have Bananas", nav[1].description)
self.assertEqual("Bananas?", nav[2].description)
self.assertEqual("LIKELY", nav[2].state)
self.assertEqual("yes", nav[2].children[0].description)
self.assertEqual("LIKELY", nav[2].children[0].state)
self.assertEqual("of Bananas", nav[2].children[0].children[0].description)
self.assertEqual("EndEvent", nav[2].children[0].children[1].spec_type)
self.assertEqual("no", nav[2].children[1].description)
self.assertEqual("MAYBE", nav[2].children[1].state)
self.assertEqual("no bananas", nav[2].children[1].children[0].description)
self.assertEqual("EndEvent", nav[2].children[1].children[1].spec_type)
def test_navigation_with_exclusive_gateway(self):
workflow = self.create_workflow('exclusive_gateway_2')
@ -130,13 +136,16 @@ class TestTasksApi(BaseTest):
self.assertIsNotNone(workflow_api.navigation)
nav = workflow_api.navigation
self.assertEqual(7, len(nav))
self.assertEqual("Task 1", nav[0]['title'])
self.assertEqual("Which Branch?", nav[1]['title'])
self.assertEqual("a", nav[2]['title'])
self.assertEqual("Task 2a", nav[3]['title'])
self.assertEqual("b", nav[4]['title'])
self.assertEqual("Task 2b", nav[5]['title'])
self.assertEqual("Task 3", nav[6]['title'])
self.assertEqual("Task 1", nav[1].description)
self.assertEqual("Which Branch?", nav[2].description)
self.assertEqual("a", nav[2].children[0].description)
self.assertEqual("Task 2a", nav[2].children[0].children[0].description)
self.assertEqual("b", nav[2].children[1].description)
self.assertEqual("Task 2b", nav[2].children[1].children[0].description)
self.assertEqual(None, nav[3].description)
self.assertEqual("Task 3", nav[4].description)
self.assertEqual("EndEvent", nav[5].spec_type)
def test_document_added_to_workflow_shows_up_in_file_list(self):
self.create_reference_document()
@ -267,7 +276,7 @@ class TestTasksApi(BaseTest):
# get the first form in the two form workflow.
workflow = self.get_workflow_api(workflow)
navigation = self.get_workflow_api(workflow).navigation
self.assertEqual(4, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual(5, len(navigation)) # Start task, form_task, multi_task, end task
self.assertEqual("UserTask", workflow.next_task.type)
self.assertEqual(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
self.assertEqual(5, workflow.next_task.multi_instance_count)
@ -385,7 +394,7 @@ class TestTasksApi(BaseTest):
navigation = workflow_api.navigation
task = workflow_api.next_task
self.assertEqual(2, len(navigation))
self.assertEqual(5, len(navigation))
self.assertEqual("UserTask", task.type)
self.assertEqual("Activity_A", task.name)
self.assertEqual("My Sub Process", task.process_name)
@ -452,8 +461,8 @@ class TestTasksApi(BaseTest):
workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow)
self.assertEqual(8, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav['state'] == "READY"]
self.assertEqual(9, len(workflow_api.navigation))
ready_items = [nav for nav in workflow_api.navigation if nav.state == "READY"]
self.assertEqual(5, len(ready_items))
self.assertEqual("UserTask", workflow_api.next_task.type)
@ -461,8 +470,8 @@ class TestTasksApi(BaseTest):
self.assertEqual("Primary Investigator", workflow_api.next_task.title)
for i in random.sample(range(5), 5):
task = TaskSchema().load(ready_items[i]['task'])
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, task.id),
task_id = ready_items[i].task_id
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, task_id),
headers=self.logged_in_headers(),
content_type="application/json")
self.assert_success(rv)
@ -470,7 +479,7 @@ class TestTasksApi(BaseTest):
workflow = WorkflowApiSchema().load(json_data)
data = workflow.next_task.data
data['investigator']['email'] = "dhf8r@virginia.edu"
self.complete_form(workflow, task, data)
self.complete_form(workflow, workflow.next_task, data)
#tasks = self.get_workflow_api(workflow).user_tasks
workflow = self.get_workflow_api(workflow)

View File

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

View File

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

156
tests/test_workflow_sync.py Normal file
View File

@ -0,0 +1,156 @@
from unittest.mock import patch
from crc import db
from tests.base_test import BaseTest
from crc.api.workflow_sync import get_all_spec_state, \
get_changed_workflows, \
get_workflow_spec_files, \
get_changed_files, \
get_workflow_specification, \
sync_changed_files
from crc.models.workflow import WorkflowSpecModel
from datetime import datetime
from crc.services.file_service import FileService
class TestWorkflowSync(BaseTest):
@patch('crc.services.workflow_sync.WorkflowSyncService.get_all_remote_workflows')
def test_get_no_changes(self, mock_get):
self.load_example_data()
othersys = get_all_spec_state()
mock_get.return_value = othersys
response = get_changed_workflows('localhost:0000') # not actually used due to mock
self.assertIsNotNone(response)
self.assertEqual(response,[])
@patch('crc.services.workflow_sync.WorkflowSyncService.get_all_remote_workflows')
def test_remote_workflow_change(self, mock_get):
self.load_example_data()
othersys = get_all_spec_state()
othersys[1]['date_created'] = str(datetime.now())
othersys[1]['md5_hash'] = '12345'
mock_get.return_value = othersys
response = get_changed_workflows('localhost:0000') #endpoint is not used due to mock
self.assertIsNotNone(response)
self.assertEqual(len(response),1)
self.assertEqual(response[0]['workflow_spec_id'], 'random_fact')
self.assertEqual(response[0]['location'], 'remote')
self.assertEqual(response[0]['new'], False)
@patch('crc.services.workflow_sync.WorkflowSyncService.get_all_remote_workflows')
def test_remote_workflow_has_new(self, mock_get):
self.load_example_data()
othersys = get_all_spec_state()
othersys.append({'workflow_spec_id':'my_new_workflow',
'date_created':str(datetime.now()),
'md5_hash': '12345'})
mock_get.return_value = othersys
response = get_changed_workflows('localhost:0000') #endpoint is not used due to mock
self.assertIsNotNone(response)
self.assertEqual(len(response),1)
self.assertEqual(response[0]['workflow_spec_id'],'my_new_workflow')
self.assertEqual(response[0]['location'], 'remote')
self.assertEqual(response[0]['new'], True)
@patch('crc.services.workflow_sync.WorkflowSyncService.get_all_remote_workflows')
def test_local_workflow_has_new(self, mock_get):
self.load_example_data()
othersys = get_all_spec_state()
mock_get.return_value = othersys
wf_spec = WorkflowSpecModel()
wf_spec.id = 'abcdefg'
wf_spec.display_name = 'New Workflow - Yum!!'
wf_spec.name = 'my_new_workflow'
wf_spec.description = 'yep - its a new workflow'
wf_spec.category_id = 0
wf_spec.display_order = 0
db.session.add(wf_spec)
db.session.commit()
FileService.add_workflow_spec_file(wf_spec,'dummyfile.txt','text',b'this is a test')
# after setting up the test - I realized that this doesn't return anything for
# a workflow that is new locally - it just returns nothing
response = get_changed_workflows('localhost:0000') #endpoint is not used due to mock
self.assertIsNotNone(response)
self.assertEqual(response,[])
@patch('crc.services.workflow_sync.WorkflowSyncService.get_remote_workflow_spec_files')
def test_file_differences(self, mock_get):
self.load_example_data()
othersys = get_workflow_spec_files('random_fact')
othersys[1]['date_created'] = str(datetime.now())
othersys[1]['md5_hash'] = '12345'
mock_get.return_value = othersys
response = get_changed_files('localhost:0000','random_fact',as_df=False) #endpoint is not used due to mock
self.assertIsNotNone(response)
self.assertEqual(len(response),1)
self.assertEqual(response[0]['filename'], 'random_fact2.bpmn')
self.assertEqual(response[0]['location'], 'remote')
self.assertEqual(response[0]['new'], False)
@patch('crc.services.workflow_sync.WorkflowSyncService.get_remote_workflow_spec_files')
def test_file_differences(self, mock_get):
self.load_example_data()
othersys = get_workflow_spec_files('random_fact')
othersys[1]['date_created'] = str(datetime.now())
othersys[1]['md5_hash'] = '12345'
mock_get.return_value = othersys
response = get_changed_files('localhost:0000','random_fact',as_df=False) #endpoint is not used due to mock
self.assertIsNotNone(response)
self.assertEqual(len(response),1)
self.assertEqual(response[0]['filename'], 'random_fact2.bpmn')
self.assertEqual(response[0]['location'], 'remote')
self.assertEqual(response[0]['new'], False)
@patch('crc.services.workflow_sync.WorkflowSyncService.get_remote_file_by_hash')
@patch('crc.services.workflow_sync.WorkflowSyncService.get_remote_workflow_spec_files')
@patch('crc.services.workflow_sync.WorkflowSyncService.get_remote_workflow_spec')
def test_file_differences(self, workflow_mock, spec_files_mock, file_data_mock):
self.load_example_data()
remote_workflow = get_workflow_specification('random_fact')
self.assertEqual(remote_workflow['display_name'],'Random Fact')
remote_workflow['description'] = 'This Workflow came from Remote'
remote_workflow['display_name'] = 'Remote Workflow'
workflow_mock.return_value = remote_workflow
othersys = get_workflow_spec_files('random_fact')
othersys[1]['date_created'] = str(datetime.now())
othersys[1]['md5_hash'] = '12345'
spec_files_mock.return_value = othersys
file_data_mock.return_value = self.workflow_sync_response('random_fact2.bpmn')
response = sync_changed_files('localhost:0000','random_fact') # endpoint not used due to mock
self.assertIsNotNone(response)
self.assertEqual(len(response),1)
self.assertEqual(response[0], 'random_fact2.bpmn')
files = FileService.get_spec_data_files('random_fact')
md5sums = [str(f.md5_hash) for f in files]
self.assertEqual('21bb6f9e-0af7-0ab2-0fc7-ec0f94787e58' in md5sums, True)
new_local_workflow = get_workflow_specification('random_fact')
self.assertEqual(new_local_workflow['display_name'],'Remote Workflow')
@patch('crc.services.workflow_sync.WorkflowSyncService.get_remote_workflow_spec_files')
@patch('crc.services.workflow_sync.WorkflowSyncService.get_remote_workflow_spec')
def test_file_deleted(self, workflow_mock, spec_files_mock):
self.load_example_data()
remote_workflow = get_workflow_specification('random_fact')
workflow_mock.return_value = remote_workflow
othersys = get_workflow_spec_files('random_fact')
del(othersys[1])
spec_files_mock.return_value = othersys
response = sync_changed_files('localhost:0000','random_fact') # endpoint not used due to mock
self.assertIsNotNone(response)
# when we delete a local file, we do not return that it was deleted - just
# a list of updated files. We may want to change this in the future.
self.assertEqual(len(response),0)
files = FileService.get_spec_data_files('random_fact')
self.assertEqual(len(files),1)

View File

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