commit
c456b3cdfd
|
@ -37,7 +37,7 @@ deploy:
|
|||
skip_cleanup: true
|
||||
on:
|
||||
all_branches: true
|
||||
condition: $TRAVIS_BRANCH =~ ^(testing|staging|master)$
|
||||
condition: $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|master|rrt\/.*)$
|
||||
|
||||
notifications:
|
||||
email:
|
||||
|
|
|
@ -478,11 +478,11 @@
|
|||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:56663fa1d5385c14c6a1236badd166d6dee987a5f64d2b6cc099dadf96eb4f09",
|
||||
"sha256:f12203bf8d94c410ab4b8d66edfde4f8a364892bde1f6747179765559f93d62a"
|
||||
"sha256:c2673233aa21dde264b84349dc2fd1dce5f30ed724a0a00e75426734de5b84ab",
|
||||
"sha256:f88fe96434b1f0f476d54224d59333eba8ca1a203a2695683c1855675c4049a7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.5.2"
|
||||
"version": "==3.6.0"
|
||||
},
|
||||
"marshmallow-enum": {
|
||||
"hashes": [
|
||||
|
@ -783,31 +783,40 @@
|
|||
"spiffworkflow": {
|
||||
"editable": true,
|
||||
"git": "https://github.com/sartography/SpiffWorkflow.git",
|
||||
"ref": "6608bb1d9cc77b906bf668804470e850ec798414"
|
||||
"ref": "29afbcd69d7bb266c7b08962b5f0b36fdbc4636b"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:083e383a1dca8384d0ea6378bd182d83c600ed4ff4ec8247d3b2442cf70db1ad",
|
||||
"sha256:0a690a6486658d03cc6a73536d46e796b6570ac1f8a7ec133f9e28c448b69828",
|
||||
"sha256:114b6ace30001f056e944cebd46daef38fdb41ebb98f5e5940241a03ed6cad43",
|
||||
"sha256:128f6179325f7597a46403dde0bf148478f868df44841348dfc8d158e00db1f9",
|
||||
"sha256:13d48cd8b925b6893a4e59b2dfb3e59a5204fd8c98289aad353af78bd214db49",
|
||||
"sha256:211a1ce7e825f7142121144bac76f53ac28b12172716a710f4bf3eab477e730b",
|
||||
"sha256:2dc57ee80b76813759cccd1a7affedf9c4dbe5b065a91fb6092c9d8151d66078",
|
||||
"sha256:3e625e283eecc15aee5b1ef77203bfb542563fa4a9aa622c7643c7b55438ff49",
|
||||
"sha256:43078c7ec0457387c79b8d52fff90a7ad352ca4c7aa841c366238c3e2cf52fdf",
|
||||
"sha256:5b1bf3c2c2dca738235ce08079783ef04f1a7fc5b21cf24adaae77f2da4e73c3",
|
||||
"sha256:6056b671aeda3fc451382e52ab8a753c0d5f66ef2a5ccc8fa5ba7abd20988b4d",
|
||||
"sha256:68d78cf4a9dfade2e6cf57c4be19f7b82ed66e67dacf93b32bb390c9bed12749",
|
||||
"sha256:7025c639ce7e170db845e94006cf5f404e243e6fc00d6c86fa19e8ad8d411880",
|
||||
"sha256:7224e126c00b8178dfd227bc337ba5e754b197a3867d33b9f30dc0208f773d70",
|
||||
"sha256:7d98e0785c4cd7ae30b4a451416db71f5724a1839025544b4edbd92e00b91f0f",
|
||||
"sha256:8d8c21e9d4efef01351bf28513648ceb988031be4159745a7ad1b3e28c8ff68a",
|
||||
"sha256:bbb545da054e6297242a1bb1ba88e7a8ffb679f518258d66798ec712b82e4e07",
|
||||
"sha256:d00b393f05dbd4ecd65c989b7f5a81110eae4baea7a6a4cdd94c20a908d1456e",
|
||||
"sha256:e18752cecaef61031252ca72031d4d6247b3212ebb84748fc5d1a0d2029c23ea"
|
||||
"sha256:128bc917ed20d78143a45024455ff0aed7d3b96772eba13d5dbaf9cc57e5c41b",
|
||||
"sha256:156a27548ba4e1fed944ff9fcdc150633e61d350d673ae7baaf6c25c04ac1f71",
|
||||
"sha256:27e2efc8f77661c9af2681755974205e7462f1ae126f498f4fe12a8b24761d15",
|
||||
"sha256:2a12f8be25b9ea3d1d5b165202181f2b7da4b3395289000284e5bb86154ce87c",
|
||||
"sha256:31c043d5211aa0e0773821fcc318eb5cbe2ec916dfbc4c6eea0c5188971988eb",
|
||||
"sha256:65eb3b03229f684af0cf0ad3bcc771970c1260a82a791a8d07bffb63d8c95bcc",
|
||||
"sha256:6cd157ce74a911325e164441ff2d9b4e244659a25b3146310518d83202f15f7a",
|
||||
"sha256:703c002277f0fbc3c04d0ae4989a174753a7554b2963c584ce2ec0cddcf2bc53",
|
||||
"sha256:869bbb637de58ab0a912b7f20e9192132f9fbc47fc6b5111cd1e0f6cdf5cf9b0",
|
||||
"sha256:8a0e0cd21da047ea10267c37caf12add400a92f0620c8bc09e4a6531a765d6d7",
|
||||
"sha256:8d01e949a5d22e5c4800d59b50617c56125fc187fbeb8fa423e99858546de616",
|
||||
"sha256:925b4fe5e7c03ed76912b75a9a41dfd682d59c0be43bce88d3b27f7f5ba028fb",
|
||||
"sha256:9cb1819008f0225a7c066cac8bb0cf90847b2c4a6eb9ebb7431dbd00c56c06c5",
|
||||
"sha256:a87d496884f40c94c85a647c385f4fd5887941d2609f71043e2b73f2436d9c65",
|
||||
"sha256:a9030cd30caf848a13a192c5e45367e3c6f363726569a56e75dc1151ee26d859",
|
||||
"sha256:a9e75e49a0f1583eee0ce93270232b8e7bb4b1edc89cc70b07600d525aef4f43",
|
||||
"sha256:b50f45d0e82b4562f59f0e0ca511f65e412f2a97d790eea5f60e34e5f1aabc9a",
|
||||
"sha256:b7878e59ec31f12d54b3797689402ee3b5cfcb5598f2ebf26491732758751908",
|
||||
"sha256:ce1ddaadee913543ff0154021d31b134551f63428065168e756d90bdc4c686f5",
|
||||
"sha256:ce2646e4c0807f3461be0653502bb48c6e91a5171d6e450367082c79e12868bf",
|
||||
"sha256:ce6c3d18b2a8ce364013d47b9cad71db815df31d55918403f8db7d890c9d07ae",
|
||||
"sha256:e4e2664232005bd306f878b0f167a31f944a07c4de0152c444f8c61bbe3cfb38",
|
||||
"sha256:e8aa395482728de8bdcca9cc0faf3765ab483e81e01923aaa736b42f0294f570",
|
||||
"sha256:eb4fcf7105bf071c71068c6eee47499ab8d4b8f5a11fc35147c934f0faa60f23",
|
||||
"sha256:ed375a79f06cad285166e5be74745df1ed6845c5624aafadec4b7a29c25866ef",
|
||||
"sha256:f35248f7e0d63b234a109dd72fbfb4b5cb6cb6840b221d0df0ecbf54ab087654",
|
||||
"sha256:f502ef245c492b391e0e23e94cba030ab91722dcc56963c85bfd7f3441ea2bbe",
|
||||
"sha256:fe01bac7226499aedf472c62fa3b85b2c619365f3f14dd222ffe4f3aa91e5f98"
|
||||
],
|
||||
"version": "==1.3.16"
|
||||
"version": "==1.3.17"
|
||||
},
|
||||
"swagger-ui-bundle": {
|
||||
"hashes": [
|
||||
|
@ -903,10 +912,10 @@
|
|||
},
|
||||
"more-itertools": {
|
||||
"hashes": [
|
||||
"sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
|
||||
"sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
|
||||
"sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be",
|
||||
"sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"
|
||||
],
|
||||
"version": "==8.2.0"
|
||||
"version": "==8.3.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
|
@ -938,11 +947,11 @@
|
|||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
|
||||
"sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
|
||||
"sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3",
|
||||
"sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.4.1"
|
||||
"version": "==5.4.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import re
|
||||
from os import environ
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
@ -6,7 +7,8 @@ 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.
|
||||
|
||||
NAME = "CR Connect Workflow"
|
||||
CORS_ENABLED = False
|
||||
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"))
|
||||
DEVELOPMENT = environ.get('DEVELOPMENT', default="true") == "true"
|
||||
TESTING = environ.get('TESTING', default="false") == "true"
|
||||
PRODUCTION = (environ.get('PRODUCTION', default="false") == "true") or (not DEVELOPMENT and not TESTING)
|
||||
|
@ -26,6 +28,7 @@ FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://l
|
|||
SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER")
|
||||
|
||||
#: Default attribute map for single signon.
|
||||
SSO_LOGIN_URL = '/login'
|
||||
SSO_ATTRIBUTE_MAP = {
|
||||
'eppn': (False, 'eppn'), # dhf8r@virginia.edu
|
||||
'uid': (True, 'uid'), # dhf8r
|
||||
|
@ -48,7 +51,10 @@ LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu")
|
|||
LDAP_TIMEOUT_SEC = environ.get('LDAP_TIMEOUT_SEC', default=3)
|
||||
print('=== USING DEFAULT CONFIG: ===')
|
||||
print('DB_HOST = ', DB_HOST)
|
||||
print('CORS_ALLOW_ORIGINS = ', CORS_ALLOW_ORIGINS)
|
||||
print('DEVELOPMENT = ', DEVELOPMENT)
|
||||
print('TESTING = ', TESTING)
|
||||
print('PRODUCTION = ', PRODUCTION)
|
||||
print('PB_BASE_URL = ', PB_BASE_URL)
|
||||
print('LDAP_URL = ', LDAP_URL)
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import os
|
|||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
NAME = "CR Connect Workflow"
|
||||
CORS_ENABLED = False
|
||||
DEVELOPMENT = True
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@localhost:5432/crc_test"
|
||||
|
|
|
@ -2,7 +2,6 @@ import os
|
|||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
NAME = "CR Connect Workflow"
|
||||
CORS_ENABLED = False
|
||||
DEVELOPMENT = True
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql://postgres:@localhost:5432/crc_test"
|
||||
|
|
|
@ -4,9 +4,10 @@ import os
|
|||
import connexion
|
||||
from flask_cors import CORS
|
||||
from flask_marshmallow import Marshmallow
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_sso import SSO
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
connexion_app = connexion.FlaskApp(__name__)
|
||||
|
@ -36,7 +37,10 @@ from crc import models
|
|||
from crc import api
|
||||
|
||||
connexion_app.add_api('api.yml')
|
||||
cors = CORS(connexion_app.app)
|
||||
|
||||
# Convert list of allowed origins to list of regexes
|
||||
origins_re = [r"^https?:\/\/%s(.*)" % o.replace('.', '\.') for o in app.config['CORS_ALLOW_ORIGINS']]
|
||||
cors = CORS(connexion_app.app, origins=origins_re)
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
|
|
59
crc/api.yml
59
crc/api.yml
|
@ -939,12 +939,10 @@ components:
|
|||
status:
|
||||
type: enum
|
||||
enum: ['new','user_input_required','waiting','complete']
|
||||
user_tasks:
|
||||
navigation:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Task"
|
||||
last_task:
|
||||
$ref: "#/components/schemas/Task"
|
||||
$ref: "#/components/schemas/NavigationItem"
|
||||
next_task:
|
||||
$ref: "#/components/schemas/Task"
|
||||
workflow_spec_id:
|
||||
|
@ -990,6 +988,19 @@ components:
|
|||
$ref: "#/components/schemas/Form"
|
||||
documentation:
|
||||
type: string
|
||||
data:
|
||||
type: object
|
||||
multi_instance_type:
|
||||
type: enum
|
||||
enum: ['none', 'looping', 'parallel', 'sequential']
|
||||
multi_instance_count:
|
||||
type: number
|
||||
multi_instance_index:
|
||||
type: number
|
||||
process_name:
|
||||
type: string
|
||||
properties:
|
||||
type: object
|
||||
example:
|
||||
id: study_identification
|
||||
name: Study Identification
|
||||
|
@ -1160,5 +1171,45 @@ components:
|
|||
example: "Chuck Norris"
|
||||
data:
|
||||
type: any
|
||||
NavigationItem:
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
format: integer
|
||||
example: 5
|
||||
task_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "1234123uuid1234"
|
||||
name:
|
||||
type: string
|
||||
example: "Task_Has_bananas"
|
||||
description:
|
||||
type: string
|
||||
example: "Has Bananas?"
|
||||
backtracks:
|
||||
type: boolean
|
||||
example: false
|
||||
level:
|
||||
type: integer
|
||||
example: 1
|
||||
indent:
|
||||
type: integer
|
||||
example: 2
|
||||
child_count:
|
||||
type: integer
|
||||
example: 4
|
||||
state:
|
||||
type: enum
|
||||
enum: ['FUTURE', 'WAITING', 'READY', 'CANCELLED', 'COMPLETED','LIKELY','MAYBE']
|
||||
readOnly: true
|
||||
is_decision:
|
||||
type: boolean
|
||||
example: False
|
||||
readOnly: true
|
||||
task:
|
||||
$ref: "#/components/schemas/Task"
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import json
|
||||
|
||||
import connexion
|
||||
from flask import redirect, g
|
||||
|
||||
|
@ -33,6 +35,7 @@ def get_current_user():
|
|||
|
||||
@sso.login_handler
|
||||
def sso_login(user_info):
|
||||
app.logger.info("Login from Shibboleth happening. " + json.dump(user_info))
|
||||
# TODO: Get redirect URL from Shibboleth request header
|
||||
_handle_login(user_info)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import uuid
|
|||
|
||||
from crc import session
|
||||
from crc.api.common import ApiError, ApiErrorSchema
|
||||
from crc.models.api_models import WorkflowApi, WorkflowApiSchema
|
||||
from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema
|
||||
from crc.models.file import FileModel, LookupDataSchema
|
||||
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \
|
||||
WorkflowSpecCategoryModelSchema
|
||||
|
@ -83,17 +83,36 @@ def delete_workflow_specification(spec_id):
|
|||
session.commit()
|
||||
|
||||
|
||||
def __get_workflow_api_model(processor: WorkflowProcessor):
|
||||
spiff_tasks = processor.get_all_user_tasks()
|
||||
user_tasks = list(map(WorkflowService.spiff_task_to_api_task, spiff_tasks))
|
||||
def __get_workflow_api_model(processor: WorkflowProcessor, next_task = None):
|
||||
"""Returns an API model representing the state of the current workflow, if requested, and
|
||||
possible, next_task is set to the current_task."""
|
||||
|
||||
nav_dict = processor.bpmn_workflow.get_nav_list()
|
||||
navigation = []
|
||||
for nav_item in nav_dict:
|
||||
spiff_task = processor.bpmn_workflow.get_task(nav_item['task_id'])
|
||||
if 'description' in nav_item:
|
||||
nav_item['title'] = nav_item.pop('description')
|
||||
# fixme: duplicate code from the workflow_service. Should only do this in one place.
|
||||
if ' ' in nav_item['title']:
|
||||
nav_item['title'] = nav_item['title'].partition(' ')[2]
|
||||
else:
|
||||
nav_item['title'] = ""
|
||||
if spiff_task:
|
||||
nav_item['task'] = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
|
||||
nav_item['title'] = nav_item['task'].title # Prefer the task title.
|
||||
else:
|
||||
nav_item['task'] = None
|
||||
if not 'is_decision' in nav_item:
|
||||
nav_item['is_decision'] = False
|
||||
|
||||
navigation.append(NavigationItem(**nav_item))
|
||||
NavigationItemSchema().dump(nav_item)
|
||||
workflow_api = WorkflowApi(
|
||||
id=processor.get_workflow_id(),
|
||||
status=processor.get_status(),
|
||||
last_task=WorkflowService.spiff_task_to_api_task(processor.bpmn_workflow.last_task),
|
||||
next_task=None,
|
||||
previous_task=processor.previous_task(),
|
||||
user_tasks=user_tasks,
|
||||
navigation=navigation,
|
||||
workflow_spec_id=processor.workflow_spec_id,
|
||||
spec_version=processor.get_spec_version(),
|
||||
is_latest_spec=processor.get_spec_version() == processor.get_latest_version_string(processor.workflow_spec_id),
|
||||
|
@ -101,7 +120,9 @@ def __get_workflow_api_model(processor: WorkflowProcessor):
|
|||
completed_tasks=processor.workflow_model.completed_tasks,
|
||||
last_updated=processor.workflow_model.last_updated
|
||||
)
|
||||
next_task = processor.next_task()
|
||||
if not next_task: # The Next Task can be requested to be a certain task, useful for parallel tasks.
|
||||
# This may or may not work, sometimes there is no next task to complete.
|
||||
next_task = processor.next_task()
|
||||
if next_task:
|
||||
workflow_api.next_task = WorkflowService.spiff_task_to_api_task(next_task, add_docs_and_forms=True)
|
||||
|
||||
|
@ -118,19 +139,20 @@ def get_workflow(workflow_id, soft_reset=False, hard_reset=False):
|
|||
def delete_workflow(workflow_id):
|
||||
StudyService.delete_workflow(workflow_id)
|
||||
|
||||
|
||||
def set_current_task(workflow_id, task_id):
|
||||
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
|
||||
processor = WorkflowProcessor(workflow_model)
|
||||
task_id = uuid.UUID(task_id)
|
||||
task = processor.bpmn_workflow.get_task(task_id)
|
||||
if task.state != task.COMPLETED:
|
||||
if task.state != task.COMPLETED and task.state != task.READY:
|
||||
raise ApiError("invalid_state", "You may not move the token to a task who's state is not "
|
||||
"currently set to COMPLETE.")
|
||||
"currently set to COMPLETE or READY.")
|
||||
|
||||
task.reset_token(reset_data=False) # we could optionally clear the previous data.
|
||||
processor.save()
|
||||
WorkflowService.log_task_action(processor, task, WorkflowService.TASK_ACTION_TOKEN_RESET)
|
||||
workflow_api_model = __get_workflow_api_model(processor)
|
||||
workflow_api_model = __get_workflow_api_model(processor, task)
|
||||
return WorkflowApiSchema().dump(workflow_api_model)
|
||||
|
||||
|
||||
|
|
|
@ -15,6 +15,20 @@ 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):
|
||||
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
|
||||
|
||||
class Task(object):
|
||||
|
||||
ENUM_OPTIONS_FILE_PROP = "enum.options.file"
|
||||
|
@ -22,9 +36,8 @@ class Task(object):
|
|||
EMUM_OPTIONS_LABEL_COL_PROP = "enum.options.label.column"
|
||||
EMUM_OPTIONS_AS_LOOKUP = "enum.options.lookup"
|
||||
|
||||
|
||||
def __init__(self, id, name, title, type, state, form, documentation, data,
|
||||
mi_type, mi_count, mi_index, process_name, properties):
|
||||
multi_instance_type, multi_instance_count, multi_instance_index, process_name, properties):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.title = title
|
||||
|
@ -33,9 +46,9 @@ class Task(object):
|
|||
self.form = form
|
||||
self.documentation = documentation
|
||||
self.data = data
|
||||
self.mi_type = mi_type # Some tasks have a repeat behavior.
|
||||
self.mi_count = mi_count # This is the number of times the task could repeat.
|
||||
self.mi_index = mi_index # And the index of the currently repeating task.
|
||||
self.multi_instance_type = multi_instance_type # Some tasks have a repeat behavior.
|
||||
self.multi_instance_count = multi_instance_count # This is the number of times the task could repeat.
|
||||
self.multi_instance_index = multi_instance_index # And the index of the currently repeating task.
|
||||
self.process_name = process_name
|
||||
self.properties = properties # Arbitrary extension properties from BPMN editor.
|
||||
|
||||
|
@ -50,10 +63,11 @@ class ValidationSchema(ma.Schema):
|
|||
fields = ["name", "config"]
|
||||
|
||||
|
||||
class PropertiesSchema(ma.Schema):
|
||||
class FormFieldPropertySchema(ma.Schema):
|
||||
class Meta:
|
||||
fields = ["id", "value"]
|
||||
|
||||
fields = [
|
||||
"id", "value"
|
||||
]
|
||||
|
||||
class FormFieldSchema(ma.Schema):
|
||||
class Meta:
|
||||
|
@ -64,7 +78,7 @@ class FormFieldSchema(ma.Schema):
|
|||
default_value = marshmallow.fields.String(required=False, allow_none=True)
|
||||
options = marshmallow.fields.List(marshmallow.fields.Nested(OptionSchema))
|
||||
validation = marshmallow.fields.List(marshmallow.fields.Nested(ValidationSchema))
|
||||
properties = marshmallow.fields.List(marshmallow.fields.Nested(PropertiesSchema))
|
||||
properties = marshmallow.fields.List(marshmallow.fields.Nested(FormFieldPropertySchema))
|
||||
|
||||
|
||||
class FormSchema(ma.Schema):
|
||||
|
@ -74,14 +88,13 @@ class FormSchema(ma.Schema):
|
|||
|
||||
class TaskSchema(ma.Schema):
|
||||
class Meta:
|
||||
fields = ["id", "name", "title", "type", "state", "form", "documentation", "data", "mi_type",
|
||||
"mi_count", "mi_index", "process_name", "properties"]
|
||||
fields = ["id", "name", "title", "type", "state", "form", "documentation", "data", "multi_instance_type",
|
||||
"multi_instance_count", "multi_instance_index", "process_name", "properties"]
|
||||
|
||||
mi_type = EnumField(MultiInstanceType)
|
||||
multi_instance_type = EnumField(MultiInstanceType)
|
||||
documentation = marshmallow.fields.String(required=False, allow_none=True)
|
||||
form = marshmallow.fields.Nested(FormSchema, required=False, allow_none=True)
|
||||
title = marshmallow.fields.String(required=False, allow_none=True)
|
||||
properties = marshmallow.fields.List(marshmallow.fields.Nested(PropertiesSchema))
|
||||
process_name = marshmallow.fields.String(required=False, allow_none=True)
|
||||
|
||||
@marshmallow.post_load
|
||||
|
@ -89,15 +102,24 @@ class TaskSchema(ma.Schema):
|
|||
return Task(**data)
|
||||
|
||||
|
||||
class NavigationItemSchema(ma.Schema):
|
||||
class Meta:
|
||||
fields = ["id", "task_id", "name", "title", "backtracks", "level", "indent", "child_count", "state",
|
||||
"is_decision", "task"]
|
||||
unknown = INCLUDE
|
||||
task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False, allow_none=True)
|
||||
backtracks = 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)
|
||||
|
||||
|
||||
class WorkflowApi(object):
|
||||
def __init__(self, id, status, user_tasks, last_task, next_task, previous_task,
|
||||
def __init__(self, id, status, next_task, navigation,
|
||||
spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, last_updated):
|
||||
self.id = id
|
||||
self.status = status
|
||||
self.user_tasks = user_tasks
|
||||
self.last_task = last_task # The last task that was completed, may be different than previous.
|
||||
self.next_task = next_task # The next task that requires user input.
|
||||
self.previous_task = previous_task # The opposite of next task.
|
||||
self.navigation = navigation
|
||||
self.workflow_spec_id = workflow_spec_id
|
||||
self.spec_version = spec_version
|
||||
self.is_latest_spec = is_latest_spec
|
||||
|
@ -108,21 +130,20 @@ class WorkflowApi(object):
|
|||
class WorkflowApiSchema(ma.Schema):
|
||||
class Meta:
|
||||
model = WorkflowApi
|
||||
fields = ["id", "status", "user_tasks", "last_task", "next_task", "previous_task",
|
||||
fields = ["id", "status", "next_task", "navigation",
|
||||
"workflow_spec_id", "spec_version", "is_latest_spec", "total_tasks", "completed_tasks",
|
||||
"last_updated"]
|
||||
unknown = INCLUDE
|
||||
|
||||
status = EnumField(WorkflowStatus)
|
||||
user_tasks = marshmallow.fields.List(marshmallow.fields.Nested(TaskSchema, dump_only=True))
|
||||
last_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False)
|
||||
next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False)
|
||||
previous_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False)
|
||||
navigation = marshmallow.fields.List(marshmallow.fields.Nested(NavigationItemSchema, dump_only=True))
|
||||
|
||||
@marshmallow.post_load
|
||||
def make_workflow(self, data, **kwargs):
|
||||
keys = ['id', 'status', 'user_tasks', 'last_task', 'next_task', 'previous_task',
|
||||
keys = ['id', 'status', 'next_task', 'navigation',
|
||||
'workflow_spec_id', 'spec_version', 'is_latest_spec', "total_tasks", "completed_tasks",
|
||||
"last_updated"]
|
||||
filtered_fields = {key: data[key] for key in keys}
|
||||
filtered_fields['next_task'] = TaskSchema().make_task(data['next_task'])
|
||||
return WorkflowApi(**filtered_fields)
|
||||
|
|
|
@ -3,8 +3,7 @@ from typing import cast
|
|||
|
||||
from marshmallow_enum import EnumField
|
||||
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
|
||||
from sqlalchemy import func, Index, text
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy import func, Index
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from crc import db
|
||||
|
|
|
@ -52,7 +52,7 @@ Takes two arguments:
|
|||
message="The CompleteTemplate script requires 2 arguments. The first argument is "
|
||||
"the name of the docx template to use. The second "
|
||||
"argument is a code for the document, as "
|
||||
"set in the reference document %s. " % FileService.IRB_PRO_CATEGORIES_FILE)
|
||||
"set in the reference document %s. " % FileService.DOCUMENT_LIST)
|
||||
task_study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
|
||||
file_name = args[0]
|
||||
|
||||
|
|
|
@ -1,39 +1,149 @@
|
|||
from ldap3.core.exceptions import LDAPSocketOpenError
|
||||
import json
|
||||
|
||||
from crc import session, app
|
||||
from crc import session
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.study import StudyModel, StudySchema
|
||||
from crc.models.workflow import WorkflowStatus
|
||||
from crc.scripts.script import Script, ScriptValidationError
|
||||
from crc.scripts.script import Script
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.ldap_service import LdapService
|
||||
from crc.services.protocol_builder import ProtocolBuilderService
|
||||
from crc.services.study_service import StudyService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
|
||||
|
||||
class StudyInfo(Script):
|
||||
"""Please see the detailed description that is provided below. """
|
||||
|
||||
"""Just your basic class that can pull in data from a few api endpoints and do a basic task."""
|
||||
pb = ProtocolBuilderService()
|
||||
type_options = ['info', 'investigators', 'details', 'approvals', 'documents', 'protocol']
|
||||
|
||||
# This is used for test/workflow validation, as well as documentation.
|
||||
example_data = {
|
||||
"StudyInfo": {
|
||||
"info": {
|
||||
"id": 12,
|
||||
"title": "test",
|
||||
"primary_investigator_id": 21,
|
||||
"user_uid": "dif84",
|
||||
"sponsor": "sponsor",
|
||||
"ind_number": "1234",
|
||||
"inactive": False
|
||||
},
|
||||
"investigators": {
|
||||
'PI': {
|
||||
'label': 'Primary Investigator',
|
||||
'display': 'Always',
|
||||
'unique': 'Yes',
|
||||
'user_id': 'dhf8r',
|
||||
'display_name': 'Dan Funk',
|
||||
'given_name': 'Dan',
|
||||
'email': 'dhf8r@virginia.edu',
|
||||
'telephone_number': '+1 (434) 924-1723',
|
||||
'title': "E42:He's a hoopy frood",
|
||||
'department': 'E0:EN-Eng Study of Parallel Universes',
|
||||
'affiliation': 'faculty',
|
||||
'sponsor_type': 'Staff'},
|
||||
'SC_I': {
|
||||
'label': 'Study Coordinator I',
|
||||
'display': 'Always',
|
||||
'unique': 'Yes',
|
||||
'user_id': None},
|
||||
'DC': {
|
||||
'label': 'Department Contact',
|
||||
'display': 'Optional',
|
||||
'unique': 'Yes',
|
||||
'user_id': 'asd3v',
|
||||
'error': 'Unable to locate a user with id asd3v in LDAP'}
|
||||
},
|
||||
"documents": {
|
||||
'AD_CoCApp': {'category1': 'Ancillary Document', 'category2': 'CoC Application', 'category3': '',
|
||||
'Who Uploads?': 'CRC', 'id': '12',
|
||||
'description': 'Certificate of Confidentiality Application', 'required': False,
|
||||
'study_id': 1, 'code': 'AD_CoCApp', 'display_name': 'Ancillary Document / CoC Application',
|
||||
'count': 0, 'files': []},
|
||||
'UVACompl_PRCAppr': {'category1': 'UVA Compliance', 'category2': 'PRC Approval', 'category3': '',
|
||||
'Who Uploads?': 'CRC', 'id': '6', 'description': "Cancer Center's PRC Approval Form",
|
||||
'required': True, 'study_id': 1, 'code': 'UVACompl_PRCAppr',
|
||||
'display_name': 'UVA Compliance / PRC Approval', 'count': 1, 'files': [
|
||||
{'file_id': 10,
|
||||
'task_id': 'fakingthisout',
|
||||
'workflow_id': 2,
|
||||
'workflow_spec_id': 'docx'}],
|
||||
'status': 'complete'}
|
||||
},
|
||||
"details":
|
||||
{},
|
||||
"approvals": {
|
||||
"study_id": 12,
|
||||
"workflow_id": 321,
|
||||
"display_name": "IRB API Details",
|
||||
"name": "irb_api_details",
|
||||
"status": WorkflowStatus.not_started.value,
|
||||
"workflow_spec_id": "irb_api_details",
|
||||
},
|
||||
'protocol': {
|
||||
id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def example_to_string(self, key):
|
||||
return json.dumps(self.example_data['StudyInfo'][key], indent=2, separators=(',', ': '))
|
||||
|
||||
def get_description(self):
|
||||
return """StudyInfo [TYPE], where TYPE is one of 'info', 'investigators', or 'details', 'approvals',
|
||||
'documents' or 'protocol'.
|
||||
Adds details about the current study to the Task Data. The type of information required should be
|
||||
provided as an argument. 'info' returns the basic information such as the title. 'Investigators' provides
|
||||
detailed information about each investigator in th study. 'Details' provides a large number
|
||||
of details about the study, as gathered within the protocol builder, and 'documents',
|
||||
lists all the documents that can be a part of the study, with documents from Protocol Builder
|
||||
marked as required, and details about any files that were uploaded' .
|
||||
"""
|
||||
return """
|
||||
StudyInfo [TYPE], where TYPE is one of 'info', 'investigators', 'details', 'approvals',
|
||||
'documents' or 'protocol'.
|
||||
|
||||
Adds details about the current study to the Task Data. The type of information required should be
|
||||
provided as an argument. The following arguments are available:
|
||||
|
||||
### Info ###
|
||||
Returns the basic information such as the id and title
|
||||
```
|
||||
{info_example}
|
||||
```
|
||||
|
||||
### Investigators ###
|
||||
Returns detailed information about related personnel.
|
||||
The order returned is guaranteed to match the order provided in the investigators.xslx reference file.
|
||||
If possible, detailed information is added in from LDAP about each personnel based on their user_id.
|
||||
```
|
||||
{investigators_example}
|
||||
```
|
||||
|
||||
### Details ###
|
||||
Returns detailed information about variable keys read in from the Protocol Builder.
|
||||
|
||||
### Approvals ###
|
||||
Returns data about the status of approvals related to a study.
|
||||
```
|
||||
{approvals_example}
|
||||
```
|
||||
|
||||
### Documents ###
|
||||
Returns a list of all documents that might be related to a study, reading all columns from the irb_documents.xsl
|
||||
file. Including information about any files that were uploaded or generated that relate to a given document.
|
||||
Please note this is just a few examples, ALL known document types are returned in an actual call.
|
||||
```
|
||||
{documents_example}
|
||||
```
|
||||
|
||||
### Protocol ###
|
||||
Returns information specific to the protocol.
|
||||
|
||||
|
||||
""".format(info_example=self.example_to_string("info"),
|
||||
investigators_example=self.example_to_string("investigators"),
|
||||
approvals_example=self.example_to_string("approvals"),
|
||||
documents_example=self.example_to_string("documents"),
|
||||
)
|
||||
|
||||
def do_task_validate_only(self, task, study_id, *args, **kwargs):
|
||||
"""For validation only, pretend no results come back from pb"""
|
||||
self.check_args(args)
|
||||
# Assure the reference file exists (a bit hacky, but we want to raise this error early, and cleanly.)
|
||||
FileService.get_file_reference_dictionary()
|
||||
FileService.get_reference_file_data(FileService.DOCUMENT_LIST)
|
||||
FileService.get_reference_file_data(FileService.INVESTIGATOR_LIST)
|
||||
data = {
|
||||
"study":{
|
||||
"info": {
|
||||
|
@ -52,7 +162,12 @@ class StudyInfo(Script):
|
|||
"NETBADGEID": "dhf8r"
|
||||
},
|
||||
"details":
|
||||
{},
|
||||
{
|
||||
"IS_IND": 0,
|
||||
"IS_IDE": 0,
|
||||
"IS_MULTI_SITE": 0,
|
||||
"IS_UVA_PI_MULTI": 0
|
||||
},
|
||||
"approvals": {
|
||||
"study_id": 12,
|
||||
"workflow_id": 321,
|
||||
|
@ -82,8 +197,7 @@ class StudyInfo(Script):
|
|||
schema = StudySchema()
|
||||
self.add_data_to_task(task, {cmd: schema.dump(study)})
|
||||
if cmd == 'investigators':
|
||||
pb_response = self.pb.get_investigators(study_id)
|
||||
self.add_data_to_task(task, {cmd: self.organize_investigators_by_type(pb_response)})
|
||||
self.add_data_to_task(task, {cmd: StudyService().get_investigators(study_id)})
|
||||
if cmd == 'details':
|
||||
self.add_data_to_task(task, {cmd: self.pb.get_study_details(study_id)})
|
||||
if cmd == 'approvals':
|
||||
|
@ -101,22 +215,3 @@ class StudyInfo(Script):
|
|||
"one of %s" % ",".join(StudyInfo.type_options))
|
||||
|
||||
|
||||
def organize_investigators_by_type(self, pb_investigators):
|
||||
"""Convert array of investigators from protocol builder into a dictionary keyed on the type"""
|
||||
output = {}
|
||||
for i in pb_investigators:
|
||||
dict = {"user_id": i["NETBADGEID"], "type_full": i["INVESTIGATORTYPEFULL"]}
|
||||
dict.update(self.get_ldap_dict_if_available(i["NETBADGEID"]))
|
||||
output[i["INVESTIGATORTYPE"]] = dict
|
||||
return output
|
||||
|
||||
def get_ldap_dict_if_available(self, user_id):
|
||||
try:
|
||||
ldap_service = LdapService()
|
||||
return ldap_service.user_info(user_id).__dict__
|
||||
except ApiError:
|
||||
app.logger.info(str(ApiError))
|
||||
return {}
|
||||
except LDAPSocketOpenError:
|
||||
app.logger.info("Failed to connect to LDAP Server.")
|
||||
return {}
|
||||
|
|
|
@ -16,7 +16,8 @@ import hashlib
|
|||
|
||||
class FileService(object):
|
||||
"""Provides consistent management and rules for storing, retrieving and processing files."""
|
||||
IRB_PRO_CATEGORIES_FILE = "irb_documents.xlsx"
|
||||
DOCUMENT_LIST = "irb_documents.xlsx"
|
||||
INVESTIGATOR_LIST = "investigators.xlsx"
|
||||
|
||||
@staticmethod
|
||||
def add_workflow_spec_file(workflow_spec: WorkflowSpecModel,
|
||||
|
@ -31,12 +32,18 @@ class FileService(object):
|
|||
|
||||
return FileService.update_file(file_model, binary_data, content_type)
|
||||
|
||||
@staticmethod
|
||||
def is_allowed_document(code):
|
||||
data_model = FileService.get_reference_file_data(FileService.DOCUMENT_LIST)
|
||||
xls = ExcelFile(data_model.data)
|
||||
df = xls.parse(xls.sheet_names[0])
|
||||
return code in df['code'].values
|
||||
|
||||
@staticmethod
|
||||
def add_form_field_file(study_id, workflow_id, task_id, form_field_key, name, content_type, binary_data):
|
||||
"""Create a new file and associate it with a user task form field within a workflow.
|
||||
Please note that the form_field_key MUST be a known file in the irb_documents.xslx reference document."""
|
||||
if not FileService.irb_document_reference_exists(form_field_key):
|
||||
if not FileService.is_allowed_document(form_field_key):
|
||||
raise ApiError("invalid_form_field_key",
|
||||
"When uploading files, the form field id must match a known document in the "
|
||||
"irb_docunents.xslx reference file. This code is not found in that file '%s'" % form_field_key)
|
||||
|
@ -52,32 +59,21 @@ class FileService(object):
|
|||
return FileService.update_file(file_model, binary_data, content_type)
|
||||
|
||||
@staticmethod
|
||||
def irb_document_reference_exists(code):
|
||||
data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE)
|
||||
def get_reference_data(reference_file_name, index_column, int_columns=[]):
|
||||
""" Opens a reference file (assumes that it is xls file) and returns the data as a
|
||||
dictionary, each row keyed on the given index_column name. If there are columns
|
||||
that should be represented as integers, pass these as an array of int_columns, lest
|
||||
you get '1.0' rather than '1' """
|
||||
data_model = FileService.get_reference_file_data(reference_file_name)
|
||||
xls = ExcelFile(data_model.data)
|
||||
df = xls.parse(xls.sheet_names[0])
|
||||
return code in df['code'].values
|
||||
|
||||
@staticmethod
|
||||
def get_file_reference_dictionary():
|
||||
"""Loads up the xsl file that contains the IRB Pro Categories and converts it to
|
||||
a Panda's data frame for processing."""
|
||||
data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE)
|
||||
xls = ExcelFile(data_model.data)
|
||||
df = xls.parse(xls.sheet_names[0])
|
||||
df['id'] = df['id'].fillna(0)
|
||||
df = df.astype({'id': 'Int64'})
|
||||
for c in int_columns:
|
||||
df[c] = df[c].fillna(0)
|
||||
df = df.astype({c: 'Int64'})
|
||||
df = df.fillna('')
|
||||
df = df.applymap(str)
|
||||
df = df.set_index('code')
|
||||
# IF we need to convert the column names to something more sensible.
|
||||
# df.columns = [snakeCase(x) for x in df.columns]
|
||||
df = df.set_index(index_column)
|
||||
return json.loads(df.to_json(orient='index'))
|
||||
# # Pandas is lovely, but weird. Here we drop records without an Id, and convert it to an integer.
|
||||
# df = df.drop_duplicates(subset='Id').astype({'Id': 'Int64'})
|
||||
# Now we index on the ID column and convert to a dictionary, where the key is the id, and the value
|
||||
# is a dictionary with all the remaining data in it. It's kinda pretty really.
|
||||
# all_dict = df.set_index('Id').to_dict('index')
|
||||
|
||||
@staticmethod
|
||||
def add_task_file(study_id, workflow_id, workflow_spec_id, task_id, name, content_type, binary_data,
|
||||
|
@ -115,12 +111,12 @@ class FileService(object):
|
|||
@staticmethod
|
||||
def update_file(file_model, binary_data, content_type):
|
||||
|
||||
file_data_model = session.query(FileDataModel).\
|
||||
file_data_model = session.query(FileDataModel). \
|
||||
filter_by(file_model_id=file_model.id,
|
||||
version=file_model.latest_version
|
||||
).with_for_update().first()
|
||||
md5_checksum = UUID(hashlib.md5(binary_data).hexdigest())
|
||||
if(file_data_model is not None and md5_checksum == file_data_model.md5_hash):
|
||||
if (file_data_model is not None and md5_checksum == file_data_model.md5_hash):
|
||||
# This file does not need to be updated, it's the same file.
|
||||
return file_model
|
||||
|
||||
|
@ -187,7 +183,6 @@ class FileService(object):
|
|||
.filter(FileDataModel.version == file_model.latest_version) \
|
||||
.first()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_reference_file_data(file_name):
|
||||
file_model = session.query(FileModel). \
|
||||
|
|
|
@ -2,9 +2,11 @@ from datetime import datetime
|
|||
import json
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
from SpiffWorkflow import WorkflowException
|
||||
from ldap3.core.exceptions import LDAPSocketOpenError
|
||||
|
||||
from crc import db, session
|
||||
from crc import db, session, app
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.file import FileModel, FileModelSchema
|
||||
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
|
||||
|
@ -13,6 +15,7 @@ from crc.models.study import StudyModel, Study, Category, WorkflowMetadata
|
|||
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
|
||||
WorkflowStatus
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.ldap_service import LdapService
|
||||
from crc.services.protocol_builder import ProtocolBuilderService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
|
||||
|
@ -108,10 +111,15 @@ class StudyService(object):
|
|||
that is available.."""
|
||||
|
||||
# Get PB required docs
|
||||
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
|
||||
try:
|
||||
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
|
||||
except requests.exceptions.ConnectionError as ce:
|
||||
app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce))
|
||||
pb_docs = []
|
||||
|
||||
# Loop through all known document types, get the counts for those files, and use pb_docs to mark those required.
|
||||
doc_dictionary = FileService.get_file_reference_dictionary()
|
||||
doc_dictionary = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id'])
|
||||
|
||||
documents = {}
|
||||
for code, doc in doc_dictionary.items():
|
||||
|
||||
|
@ -149,6 +157,37 @@ class StudyService(object):
|
|||
return documents
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_investigators(study_id):
|
||||
|
||||
# Loop through all known investigator types as set in the reference file
|
||||
inv_dictionary = FileService.get_reference_data(FileService.INVESTIGATOR_LIST, 'code')
|
||||
|
||||
# Get PB required docs
|
||||
pb_investigators = ProtocolBuilderService.get_investigators(study_id=study_id)
|
||||
|
||||
"""Convert array of investigators from protocol builder into a dictionary keyed on the type"""
|
||||
for i_type in inv_dictionary:
|
||||
pb_data = next((item for item in pb_investigators if item['INVESTIGATORTYPE'] == i_type), None)
|
||||
if pb_data:
|
||||
inv_dictionary[i_type]['user_id'] = pb_data["NETBADGEID"]
|
||||
inv_dictionary[i_type].update(StudyService.get_ldap_dict_if_available(pb_data["NETBADGEID"]))
|
||||
else:
|
||||
inv_dictionary[i_type]['user_id'] = None
|
||||
|
||||
return inv_dictionary
|
||||
|
||||
@staticmethod
|
||||
def get_ldap_dict_if_available(user_id):
|
||||
try:
|
||||
ldap_service = LdapService()
|
||||
return ldap_service.user_info(user_id).__dict__
|
||||
except ApiError as ae:
|
||||
app.logger.info(str(ae))
|
||||
return {"error": str(ae)}
|
||||
except LDAPSocketOpenError:
|
||||
app.logger.info("Failed to connect to LDAP Server.")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_protocol(study_id):
|
||||
|
|
|
@ -4,7 +4,7 @@ import string
|
|||
import xml.etree.ElementTree as ElementTree
|
||||
from datetime import datetime
|
||||
|
||||
from SpiffWorkflow import Task as SpiffTask
|
||||
from SpiffWorkflow import Task as SpiffTask, WorkflowException
|
||||
from SpiffWorkflow.bpmn.BpmnScriptEngine import BpmnScriptEngine
|
||||
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
|
||||
from SpiffWorkflow.bpmn.serializer.BpmnSerializer import BpmnSerializer
|
||||
|
@ -12,6 +12,7 @@ from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
|
|||
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
|
||||
from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser
|
||||
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
|
||||
from SpiffWorkflow.exceptions import WorkflowTaskExecException
|
||||
from SpiffWorkflow.operators import Operator
|
||||
from SpiffWorkflow.specs import WorkflowSpec
|
||||
|
||||
|
@ -69,24 +70,6 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
|
|||
camel = camel.strip()
|
||||
return re.sub(r'(?<!^)(?=[A-Z])', '_', camel).lower()
|
||||
|
||||
def evaluate(self, task, expression):
|
||||
"""
|
||||
Evaluate the given expression, within the context of the given task and
|
||||
return the result.
|
||||
"""
|
||||
if isinstance(expression, Operator):
|
||||
return expression._matches(task)
|
||||
else:
|
||||
return self._eval(task, expression, **task.data)
|
||||
|
||||
def _eval(self, task, expression, **kwargs):
|
||||
locals().update(kwargs)
|
||||
try:
|
||||
return eval(expression)
|
||||
except NameError as ne:
|
||||
raise ApiError.from_task('invalid_expression',
|
||||
"The expression '%s' you provided has a missing value. % s" % (expression, str(ne)),
|
||||
task=task)
|
||||
|
||||
class MyCustomParser(BpmnDmnParser):
|
||||
"""
|
||||
|
@ -180,10 +163,14 @@ class WorkflowProcessor(object):
|
|||
Useful for running the master specification, which should not persist. """
|
||||
version = WorkflowProcessor.get_latest_version_string(spec_model.id)
|
||||
spec = WorkflowProcessor.get_spec(spec_model.id, version)
|
||||
bpmn_workflow = BpmnWorkflow(spec, script_engine=WorkflowProcessor._script_engine)
|
||||
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study.id
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = False
|
||||
bpmn_workflow.do_engine_steps()
|
||||
try:
|
||||
bpmn_workflow = BpmnWorkflow(spec, script_engine=WorkflowProcessor._script_engine)
|
||||
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study.id
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = False
|
||||
bpmn_workflow.do_engine_steps()
|
||||
except WorkflowException as we:
|
||||
raise ApiError.from_task_spec("error_running_master_spec", str(we), we.sender)
|
||||
|
||||
if not bpmn_workflow.is_completed():
|
||||
raise ApiError("master_spec_not_automatic",
|
||||
"The master spec should only contain fully automated tasks, it failed to complete.")
|
||||
|
@ -282,13 +269,13 @@ class WorkflowProcessor(object):
|
|||
|
||||
|
||||
@staticmethod
|
||||
def populate_form_with_random_data(task):
|
||||
def populate_form_with_random_data(task, task_api):
|
||||
"""populates a task with random data - useful for testing a spec."""
|
||||
|
||||
if not hasattr(task.task_spec, 'form'): return
|
||||
|
||||
form_data = {}
|
||||
for field in task.task_spec.form.fields:
|
||||
for field in task_api.form.fields:
|
||||
if field.type == "enum":
|
||||
if len(field.options) > 0:
|
||||
form_data[field.id] = random.choice(field.options)
|
||||
|
@ -346,7 +333,10 @@ class WorkflowProcessor(object):
|
|||
return self.workflow_model.spec_version
|
||||
|
||||
def do_engine_steps(self):
|
||||
self.bpmn_workflow.do_engine_steps()
|
||||
try:
|
||||
self.bpmn_workflow.do_engine_steps()
|
||||
except WorkflowTaskExecException as we:
|
||||
raise ApiError.from_task("task_error", str(we), we.task)
|
||||
|
||||
def serialize(self):
|
||||
return self._serializer.serialize_workflow(self.bpmn_workflow)
|
||||
|
@ -402,6 +392,17 @@ class WorkflowProcessor(object):
|
|||
def get_ready_user_tasks(self):
|
||||
return self.bpmn_workflow.get_ready_user_tasks()
|
||||
|
||||
def get_current_user_tasks(self):
|
||||
"""Return a list of all user tasks that are READY or
|
||||
COMPLETE and are parallel to the READY Task."""
|
||||
ready_tasks = self.bpmn_workflow.get_ready_user_tasks()
|
||||
additional_tasks = []
|
||||
if len(ready_tasks) > 0:
|
||||
for child in ready_tasks[0].parent.children:
|
||||
if child.state == SpiffTask.COMPLETED:
|
||||
additional_tasks.append(child)
|
||||
return ready_tasks + additional_tasks
|
||||
|
||||
def get_all_user_tasks(self):
|
||||
all_tasks = self.bpmn_workflow.get_tasks(SpiffTask.ANY_MASK)
|
||||
return [t for t in all_tasks if not self.bpmn_workflow._is_engine_task(t.task_spec)]
|
||||
|
@ -435,5 +436,8 @@ class WorkflowProcessor(object):
|
|||
|
||||
return process_elements[0].attrib['id']
|
||||
|
||||
|
||||
def get_nav_item(self, task):
|
||||
for nav_item in self.bpmn_workflow.get_nav_list():
|
||||
if nav_item['task_id'] == task.id:
|
||||
return nav_item
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ from flask import g
|
|||
from pandas import ExcelFile
|
||||
from sqlalchemy import func
|
||||
|
||||
from crc import db
|
||||
from crc import db, app
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.api_models import Task, MultiInstanceType
|
||||
import jinja2
|
||||
|
@ -24,7 +24,6 @@ from SpiffWorkflow import Task as SpiffTask, WorkflowException
|
|||
|
||||
|
||||
class WorkflowService(object):
|
||||
|
||||
TASK_ACTION_COMPLETE = "Complete"
|
||||
TASK_ACTION_TOKEN_RESET = "Backwards Move"
|
||||
TASK_ACTION_HARD_RESET = "Restart (Hard)"
|
||||
|
@ -54,8 +53,9 @@ class WorkflowService(object):
|
|||
tasks = bpmn_workflow.get_tasks(SpiffTask.READY)
|
||||
for task in tasks:
|
||||
task_api = WorkflowService.spiff_task_to_api_task(
|
||||
task) # Assure we try to process the documenation, and raise those errors.
|
||||
WorkflowProcessor.populate_form_with_random_data(task)
|
||||
task,
|
||||
add_docs_and_forms=True) # Assure we try to process the documenation, and raise those errors.
|
||||
WorkflowProcessor.populate_form_with_random_data(task, task_api)
|
||||
task.complete()
|
||||
except WorkflowException as we:
|
||||
raise ApiError.from_task_spec("workflow_execution_exception", str(we),
|
||||
|
@ -90,10 +90,10 @@ class WorkflowService(object):
|
|||
else:
|
||||
mi_type = MultiInstanceType.none
|
||||
|
||||
props = []
|
||||
props = {}
|
||||
if hasattr(spiff_task.task_spec, 'extensions'):
|
||||
for id, val in spiff_task.task_spec.extensions.items():
|
||||
props.append({"id": id, "value": val})
|
||||
props[id] = val
|
||||
|
||||
task = Task(spiff_task.id,
|
||||
spiff_task.task_spec.name,
|
||||
|
@ -102,25 +102,52 @@ class WorkflowService(object):
|
|||
spiff_task.get_state_name(),
|
||||
None,
|
||||
"",
|
||||
spiff_task.data,
|
||||
{},
|
||||
mi_type,
|
||||
info["mi_count"],
|
||||
info["mi_index"],
|
||||
process_name=spiff_task.task_spec._wf_spec.description,
|
||||
properties=props)
|
||||
properties=props
|
||||
)
|
||||
|
||||
# Only process the form and documentation if requested.
|
||||
# The task should be in a completed or a ready state, and should
|
||||
# not be a previously completed MI Task.
|
||||
if add_docs_and_forms:
|
||||
task.data = spiff_task.data
|
||||
if hasattr(spiff_task.task_spec, "form"):
|
||||
task.form = spiff_task.task_spec.form
|
||||
for field in task.form.fields:
|
||||
WorkflowService.process_options(spiff_task, field)
|
||||
|
||||
task.documentation = WorkflowService._process_documentation(spiff_task)
|
||||
|
||||
# All ready tasks should have a valid name, and this can be computed for
|
||||
# some tasks, particularly multi-instance tasks that all have the same spec
|
||||
# but need different labels.
|
||||
if spiff_task.state == SpiffTask.READY:
|
||||
task.properties = WorkflowService._process_properties(spiff_task, props)
|
||||
|
||||
# Replace the title with the display name if it is set in the task properties,
|
||||
# otherwise strip off the first word of the task, as that should be following
|
||||
# a BPMN standard, and should not be included in the display.
|
||||
if task.properties and "display_name" in task.properties:
|
||||
task.title = task.properties['display_name']
|
||||
elif task.title and ' ' in task.title:
|
||||
task.title = task.title.partition(' ')[2]
|
||||
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def _process_properties(spiff_task, props):
|
||||
"""Runs all the property values through the Jinja2 processor to inject data."""
|
||||
for k, v in props.items():
|
||||
try:
|
||||
template = Template(v)
|
||||
props[k] = template.render(**spiff_task.data)
|
||||
except jinja2.exceptions.TemplateError as ue:
|
||||
app.logger.error("Failed to process task property %s " % str(ue))
|
||||
return props
|
||||
|
||||
@staticmethod
|
||||
def _process_documentation(spiff_task):
|
||||
"""Runs the given documentation string through the Jinja2 processor to inject data
|
||||
|
@ -144,20 +171,22 @@ class WorkflowService(object):
|
|||
return template.render(**spiff_task.data)
|
||||
except jinja2.exceptions.TemplateError as ue:
|
||||
|
||||
# return "Error processing template. %s" % ue.message
|
||||
# return "Error processing template. %s" % ue.message
|
||||
raise ApiError(code="template_error", message="Error processing template for task %s: %s" %
|
||||
(spiff_task.task_spec.name, str(ue)), status_code=500)
|
||||
# TODO: Catch additional errors and report back.
|
||||
|
||||
@staticmethod
|
||||
def process_options(spiff_task, field):
|
||||
lookup_model = WorkflowService.get_lookup_table(spiff_task, field);
|
||||
lookup_model = WorkflowService.get_lookup_table(spiff_task, field)
|
||||
|
||||
# If lookup is set to true, do not populate options, a lookup will happen later.
|
||||
if field.has_property(Task.EMUM_OPTIONS_AS_LOOKUP) and field.get_property(Task.EMUM_OPTIONS_AS_LOOKUP):
|
||||
pass
|
||||
else:
|
||||
data = db.session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_model).all()
|
||||
if not hasattr(field, 'options'):
|
||||
field.options = []
|
||||
for d in data:
|
||||
field.options.append({"id": d.value, "name": d.label})
|
||||
|
||||
|
@ -259,14 +288,11 @@ class WorkflowService(object):
|
|||
task_title=task.title,
|
||||
task_type=str(task.type),
|
||||
task_state=task.state,
|
||||
mi_type=task.mi_type.value, # Some tasks have a repeat behavior.
|
||||
mi_count=task.mi_count, # This is the number of times the task could repeat.
|
||||
mi_index=task.mi_index, # And the index of the currently repeating task.
|
||||
mi_type=task.multi_instance_type.value, # Some tasks have a repeat behavior.
|
||||
mi_count=task.multi_instance_count, # This is the number of times the task could repeat.
|
||||
mi_index=task.multi_instance_index, # And the index of the currently repeating task.
|
||||
process_name=task.process_name,
|
||||
date=datetime.now(),
|
||||
)
|
||||
db.session.add(task_event)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1wv9t3c" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1wv9t3c" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||
<bpmn:process id="Process_19ej1y2" name="Data Securty Plan" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1co48s3">
|
||||
<bpmn:outgoing>SequenceFlow_100w7co</bpmn:outgoing>
|
||||
|
@ -372,7 +372,7 @@
|
|||
<bpmn:incoming>SequenceFlow_0nc6lcs</bpmn:incoming>
|
||||
<bpmn:outgoing>SequenceFlow_0gp2pjm</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:sequenceFlow id="SequenceFlow_0gp2pjm" sourceRef="Task_EnterIndividualUseDevices" targetRef="Task_EnterOutsideUVA" />
|
||||
<bpmn:sequenceFlow id="SequenceFlow_0gp2pjm" sourceRef="Task_EnterIndividualUseDevices" targetRef="Activity_1qbfs1w" />
|
||||
<bpmn:sequenceFlow id="SequenceFlow_0mgwas4" sourceRef="Task_EnterOutsideUVA" targetRef="ExclusiveGateway_0pi0c2d" />
|
||||
<bpmn:sequenceFlow id="SequenceFlow_1i8e52t" sourceRef="ExclusiveGateway_0x3t2vl" targetRef="Task_EnterEmailMethods" />
|
||||
<bpmn:userTask id="Task_EnterOutsideUVA" name="Enter Outside of UVA" camunda:formKey="EnterOutsideUVa">
|
||||
|
@ -389,7 +389,7 @@ Indicate all the possible formats in which you will transmit your data outside o
|
|||
</camunda:formField>
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>SequenceFlow_0gp2pjm</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_0cpwkms</bpmn:incoming>
|
||||
<bpmn:outgoing>SequenceFlow_0mgwas4</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:userTask id="Task_EnterEmailMethods" name="Enter Email Methods" camunda:formKey="EnterEmailMethods">
|
||||
|
@ -623,222 +623,339 @@ Indicate all the possible formats in which you will collect or receive your orig
|
|||
<bpmn:incoming>SequenceFlow_0blyor8</bpmn:incoming>
|
||||
<bpmn:outgoing>SequenceFlow_1oq4w2h</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:task id="Activity_1qbfs1w" name="Enter Device Details">
|
||||
<bpmn:incoming>SequenceFlow_0gp2pjm</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0cpwkms</bpmn:outgoing>
|
||||
<bpmn:multiInstanceLoopCharacteristics />
|
||||
</bpmn:task>
|
||||
<bpmn:sequenceFlow id="Flow_0cpwkms" sourceRef="Activity_1qbfs1w" targetRef="Task_EnterOutsideUVA" />
|
||||
<bpmn:textAnnotation id="TextAnnotation_190dbhy">
|
||||
<bpmn:text>> Instructions
|
||||
o Hippa Instructions
|
||||
o Hippa Indentifiers
|
||||
o Vuew Definitions and Instructions
|
||||
o Paper Documents
|
||||
o Emailed to UVA Personnel
|
||||
o EMC (EPIC)
|
||||
o UVA Approvled eCRF
|
||||
o UVA Servers
|
||||
o Web or Cloud Server
|
||||
o Individual Use Devices
|
||||
o Device Details
|
||||
0 Outside of UVA
|
||||
|
||||
o Outside of UVA?
|
||||
o Yes
|
||||
o Email Methods
|
||||
o Data Management
|
||||
o Transmission Method
|
||||
o Generate DSP
|
||||
o No
|
||||
o Generate DSP</bpmn:text>
|
||||
</bpmn:textAnnotation>
|
||||
<bpmn:association id="Association_1nrg5es" sourceRef="Task_0q6ir2l" targetRef="TextAnnotation_190dbhy" />
|
||||
<bpmn:textAnnotation id="TextAnnotation_0l6dohi">
|
||||
<bpmn:text>* Instructions
|
||||
* Hippa Instructions
|
||||
* Hippa Indentifiers
|
||||
o Vuew Definitions and Instructions
|
||||
>> Paper Documents
|
||||
> Emailed to UVA Personnel
|
||||
> EMC (EPIC)
|
||||
> UVA Approvled eCRF
|
||||
> UVA Servers
|
||||
> Web or Cloud Server
|
||||
o Individual Use Devices
|
||||
o Device Details
|
||||
o Outside of UVA
|
||||
|
||||
o Outside of UVA?
|
||||
o Yes
|
||||
o Email Methods
|
||||
o Data Management
|
||||
o Transmission Method
|
||||
o Generate DSP
|
||||
o No
|
||||
o Generate DSP</bpmn:text>
|
||||
</bpmn:textAnnotation>
|
||||
<bpmn:association id="Association_0ykpfju" sourceRef="Task_EnterPaperDocuments" targetRef="TextAnnotation_0l6dohi" />
|
||||
<bpmn:textAnnotation id="TextAnnotation_0tpe506">
|
||||
<bpmn:text>* Instructions
|
||||
* Hippa Instructions
|
||||
* Hippa Indentifiers
|
||||
* View Definitions and Instructions
|
||||
|
||||
|
||||
* Paper Documents (Parallel creates spaces)
|
||||
* Emailed to UVA Personnel
|
||||
* EMC (EPIC)
|
||||
* UVA Approvled eCRF
|
||||
* UVA Servers
|
||||
* Web or Cloud Server
|
||||
* Individual Use Devices
|
||||
|
||||
o Device Details (MultiInstance Indents, Parallel creates spaces))
|
||||
> Desktop
|
||||
>> Laptop
|
||||
> Cell Phone
|
||||
> Other
|
||||
|
||||
o Outside of UVA
|
||||
|
||||
o Outside of UVA?
|
||||
o Yes
|
||||
o Email Methods
|
||||
o Data Management
|
||||
o Transmission Method
|
||||
o Generate DSP
|
||||
o No
|
||||
o Generate DSP</bpmn:text>
|
||||
</bpmn:textAnnotation>
|
||||
<bpmn:association id="Association_01x19xx" sourceRef="Activity_1qbfs1w" targetRef="TextAnnotation_0tpe506" />
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_19ej1y2">
|
||||
<bpmndi:BPMNShape id="TextAnnotation_0l6dohi_di" bpmnElement="TextAnnotation_0l6dohi">
|
||||
<dc:Bounds x="1200" y="80" width="283.5273279352227" height="309.04183535762485" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="TextAnnotation_190dbhy_di" bpmnElement="TextAnnotation_190dbhy">
|
||||
<dc:Bounds x="190" y="150" width="237.9175101214575" height="309.04183535762485" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="TextAnnotation_0tpe506_di" bpmnElement="TextAnnotation_0tpe506">
|
||||
<dc:Bounds x="1240" y="820" width="370" height="442" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_16kyite_di" bpmnElement="SequenceFlow_16kyite">
|
||||
<di:waypoint x="2240" y="390" />
|
||||
<di:waypoint x="2322" y="390" />
|
||||
<di:waypoint x="2240" y="660" />
|
||||
<di:waypoint x="2322" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0t6xl9i_di" bpmnElement="SequenceFlow_0t6xl9i">
|
||||
<di:waypoint x="1620" y="415" />
|
||||
<di:waypoint x="1620" y="640" />
|
||||
<di:waypoint x="2190" y="640" />
|
||||
<di:waypoint x="2190" y="430" />
|
||||
<di:waypoint x="1620" y="685" />
|
||||
<di:waypoint x="1620" y="910" />
|
||||
<di:waypoint x="2190" y="910" />
|
||||
<di:waypoint x="2190" y="700" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0k2r83n_di" bpmnElement="SequenceFlow_0k2r83n">
|
||||
<di:waypoint x="2075" y="390" />
|
||||
<di:waypoint x="2140" y="390" />
|
||||
<di:waypoint x="2075" y="660" />
|
||||
<di:waypoint x="2140" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0blyor8_di" bpmnElement="SequenceFlow_0blyor8">
|
||||
<di:waypoint x="665" y="390" />
|
||||
<di:waypoint x="717" y="390" />
|
||||
<di:waypoint x="665" y="660" />
|
||||
<di:waypoint x="717" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0jyty9m_di" bpmnElement="SequenceFlow_0jyty9m">
|
||||
<di:waypoint x="498" y="390" />
|
||||
<di:waypoint x="565" y="390" />
|
||||
<di:waypoint x="498" y="660" />
|
||||
<di:waypoint x="565" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0m2op9s_di" bpmnElement="SequenceFlow_0m2op9s">
|
||||
<di:waypoint x="351" y="390" />
|
||||
<di:waypoint x="398" y="390" />
|
||||
<di:waypoint x="351" y="660" />
|
||||
<di:waypoint x="398" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1oq4w2h_di" bpmnElement="SequenceFlow_1oq4w2h">
|
||||
<di:waypoint x="817" y="390" />
|
||||
<di:waypoint x="875" y="390" />
|
||||
<di:waypoint x="817" y="660" />
|
||||
<di:waypoint x="875" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_100w7co_di" bpmnElement="SequenceFlow_100w7co">
|
||||
<di:waypoint x="191" y="390" />
|
||||
<di:waypoint x="251" y="390" />
|
||||
<di:waypoint x="191" y="660" />
|
||||
<di:waypoint x="251" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_01hl869_di" bpmnElement="SequenceFlow_01hl869">
|
||||
<di:waypoint x="1645" y="390" />
|
||||
<di:waypoint x="1725" y="390" />
|
||||
<di:waypoint x="1645" y="660" />
|
||||
<di:waypoint x="1725" y="660" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1676" y="372" width="18" height="14" />
|
||||
<dc:Bounds x="1676" y="642" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0lere0k_di" bpmnElement="SequenceFlow_0lere0k">
|
||||
<di:waypoint x="1950" y="530" />
|
||||
<di:waypoint x="2050" y="530" />
|
||||
<di:waypoint x="2050" y="415" />
|
||||
<di:waypoint x="1950" y="800" />
|
||||
<di:waypoint x="2050" y="800" />
|
||||
<di:waypoint x="2050" y="685" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0uewki3_di" bpmnElement="SequenceFlow_0uewki3">
|
||||
<di:waypoint x="1950" y="250" />
|
||||
<di:waypoint x="2050" y="250" />
|
||||
<di:waypoint x="2050" y="365" />
|
||||
<di:waypoint x="1950" y="520" />
|
||||
<di:waypoint x="2050" y="520" />
|
||||
<di:waypoint x="2050" y="635" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_08rwbhm_di" bpmnElement="SequenceFlow_08rwbhm">
|
||||
<di:waypoint x="1950" y="390" />
|
||||
<di:waypoint x="2025" y="390" />
|
||||
<di:waypoint x="1950" y="660" />
|
||||
<di:waypoint x="2025" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1mnmo6p_di" bpmnElement="SequenceFlow_1mnmo6p">
|
||||
<di:waypoint x="1750" y="415" />
|
||||
<di:waypoint x="1750" y="530" />
|
||||
<di:waypoint x="1850" y="530" />
|
||||
<di:waypoint x="1750" y="685" />
|
||||
<di:waypoint x="1750" y="800" />
|
||||
<di:waypoint x="1850" y="800" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_12bv2i4_di" bpmnElement="SequenceFlow_12bv2i4">
|
||||
<di:waypoint x="1775" y="390" />
|
||||
<di:waypoint x="1850" y="390" />
|
||||
<di:waypoint x="1775" y="660" />
|
||||
<di:waypoint x="1850" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1i8e52t_di" bpmnElement="SequenceFlow_1i8e52t">
|
||||
<di:waypoint x="1750" y="365" />
|
||||
<di:waypoint x="1750" y="250" />
|
||||
<di:waypoint x="1850" y="250" />
|
||||
<di:waypoint x="1750" y="635" />
|
||||
<di:waypoint x="1750" y="520" />
|
||||
<di:waypoint x="1850" y="520" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0mgwas4_di" bpmnElement="SequenceFlow_0mgwas4">
|
||||
<di:waypoint x="1550" y="390" />
|
||||
<di:waypoint x="1595" y="390" />
|
||||
<di:waypoint x="1570" y="660" />
|
||||
<di:waypoint x="1595" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0gp2pjm_di" bpmnElement="SequenceFlow_0gp2pjm">
|
||||
<di:waypoint x="1380" y="390" />
|
||||
<di:waypoint x="1450" y="390" />
|
||||
<di:waypoint x="1300" y="660" />
|
||||
<di:waypoint x="1330" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0nc6lcs_di" bpmnElement="SequenceFlow_0nc6lcs">
|
||||
<di:waypoint x="1185" y="390" />
|
||||
<di:waypoint x="1280" y="390" />
|
||||
<di:waypoint x="1185" y="660" />
|
||||
<di:waypoint x="1200" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_10fsxk4_di" bpmnElement="SequenceFlow_10fsxk4">
|
||||
<di:waypoint x="1080" y="650" />
|
||||
<di:waypoint x="1160" y="650" />
|
||||
<di:waypoint x="1160" y="415" />
|
||||
<di:waypoint x="1080" y="910" />
|
||||
<di:waypoint x="1160" y="910" />
|
||||
<di:waypoint x="1160" y="685" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1xp62py_di" bpmnElement="SequenceFlow_1xp62py">
|
||||
<di:waypoint x="1080" y="550" />
|
||||
<di:waypoint x="1160" y="550" />
|
||||
<di:waypoint x="1160" y="415" />
|
||||
<di:waypoint x="1080" y="810" />
|
||||
<di:waypoint x="1160" y="810" />
|
||||
<di:waypoint x="1160" y="685" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1agmshr_di" bpmnElement="SequenceFlow_1agmshr">
|
||||
<di:waypoint x="1080" y="440" />
|
||||
<di:waypoint x="1160" y="440" />
|
||||
<di:waypoint x="1160" y="415" />
|
||||
<di:waypoint x="1080" y="710" />
|
||||
<di:waypoint x="1160" y="710" />
|
||||
<di:waypoint x="1160" y="685" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_12cos7w_di" bpmnElement="SequenceFlow_12cos7w">
|
||||
<di:waypoint x="1080" y="220" />
|
||||
<di:waypoint x="1160" y="220" />
|
||||
<di:waypoint x="1160" y="365" />
|
||||
<di:waypoint x="1080" y="490" />
|
||||
<di:waypoint x="1160" y="490" />
|
||||
<di:waypoint x="1160" y="635" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1q6gf6w_di" bpmnElement="SequenceFlow_1q6gf6w">
|
||||
<di:waypoint x="1080" y="120" />
|
||||
<di:waypoint x="1160" y="120" />
|
||||
<di:waypoint x="1160" y="365" />
|
||||
<di:waypoint x="1080" y="390" />
|
||||
<di:waypoint x="1160" y="390" />
|
||||
<di:waypoint x="1160" y="635" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0z10m1d_di" bpmnElement="SequenceFlow_0z10m1d">
|
||||
<di:waypoint x="1080" y="320" />
|
||||
<di:waypoint x="1160" y="320" />
|
||||
<di:waypoint x="1160" y="365" />
|
||||
<di:waypoint x="1080" y="590" />
|
||||
<di:waypoint x="1160" y="590" />
|
||||
<di:waypoint x="1160" y="635" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0obqjjx_di" bpmnElement="SequenceFlow_0obqjjx">
|
||||
<di:waypoint x="900" y="415" />
|
||||
<di:waypoint x="900" y="650" />
|
||||
<di:waypoint x="980" y="650" />
|
||||
<di:waypoint x="900" y="685" />
|
||||
<di:waypoint x="900" y="910" />
|
||||
<di:waypoint x="980" y="910" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0ng3fm8_di" bpmnElement="SequenceFlow_0ng3fm8">
|
||||
<di:waypoint x="900" y="415" />
|
||||
<di:waypoint x="900" y="550" />
|
||||
<di:waypoint x="980" y="550" />
|
||||
<di:waypoint x="900" y="685" />
|
||||
<di:waypoint x="900" y="810" />
|
||||
<di:waypoint x="980" y="810" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0pw57x9_di" bpmnElement="SequenceFlow_0pw57x9">
|
||||
<di:waypoint x="900" y="415" />
|
||||
<di:waypoint x="900" y="440" />
|
||||
<di:waypoint x="980" y="440" />
|
||||
<di:waypoint x="900" y="685" />
|
||||
<di:waypoint x="900" y="710" />
|
||||
<di:waypoint x="980" y="710" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_10g92nf_di" bpmnElement="SequenceFlow_10g92nf">
|
||||
<di:waypoint x="900" y="365" />
|
||||
<di:waypoint x="900" y="320" />
|
||||
<di:waypoint x="980" y="320" />
|
||||
<di:waypoint x="900" y="635" />
|
||||
<di:waypoint x="900" y="590" />
|
||||
<di:waypoint x="980" y="590" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_084dyht_di" bpmnElement="SequenceFlow_084dyht">
|
||||
<di:waypoint x="900" y="365" />
|
||||
<di:waypoint x="900" y="220" />
|
||||
<di:waypoint x="980" y="220" />
|
||||
<di:waypoint x="900" y="635" />
|
||||
<di:waypoint x="900" y="490" />
|
||||
<di:waypoint x="980" y="490" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1i6ac9a_di" bpmnElement="SequenceFlow_1i6ac9a">
|
||||
<di:waypoint x="900" y="365" />
|
||||
<di:waypoint x="900" y="120" />
|
||||
<di:waypoint x="980" y="120" />
|
||||
<di:waypoint x="900" y="635" />
|
||||
<di:waypoint x="900" y="390" />
|
||||
<di:waypoint x="980" y="390" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0cpwkms_di" bpmnElement="Flow_0cpwkms">
|
||||
<di:waypoint x="1430" y="660" />
|
||||
<di:waypoint x="1470" y="660" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="StartEvent_1co48s3_di" bpmnElement="StartEvent_1co48s3">
|
||||
<dc:Bounds x="155" y="372" width="36" height="36" />
|
||||
<dc:Bounds x="155" y="642" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_16imtaa_di" bpmnElement="Task_EnterHIPAAIdentifiers">
|
||||
<dc:Bounds x="565" y="350" width="100" height="80" />
|
||||
<dc:Bounds x="565" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ParallelGateway_03qblyb_di" bpmnElement="ExclusiveGateway_0b16kmf">
|
||||
<dc:Bounds x="875" y="365" width="50" height="50" />
|
||||
<dc:Bounds x="875" y="635" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_1wsga3m_di" bpmnElement="Task_EnterPaperDocuments">
|
||||
<dc:Bounds x="980" y="80" width="100" height="80" />
|
||||
<dc:Bounds x="980" y="350" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0o1xjub_di" bpmnElement="Task_EnterEmailedUVAPersonnel">
|
||||
<dc:Bounds x="980" y="180" width="100" height="80" />
|
||||
<dc:Bounds x="980" y="450" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_1gnbchf_di" bpmnElement="Task_EnterEMR">
|
||||
<dc:Bounds x="980" y="280" width="100" height="80" />
|
||||
<dc:Bounds x="980" y="550" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0a4bj92_di" bpmnElement="Task_EnterUVAApprovedECRF">
|
||||
<dc:Bounds x="980" y="400" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_1f2b80a_di" bpmnElement="Task_EnterUVaServersWebsites">
|
||||
<dc:Bounds x="980" y="510" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0n3jbd7_di" bpmnElement="Task_EnterWebCloudServer">
|
||||
<dc:Bounds x="980" y="610" width="100" height="80" />
|
||||
<dc:Bounds x="980" y="670" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ParallelGateway_0zl5t7b_di" bpmnElement="ExclusiveGateway_06kvl84">
|
||||
<dc:Bounds x="1135" y="365" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0q8o038_di" bpmnElement="Task_EnterIndividualUseDevices">
|
||||
<dc:Bounds x="1280" y="350" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_10x8kgc_di" bpmnElement="Task_EnterOutsideUVA">
|
||||
<dc:Bounds x="1450" y="350" width="100" height="80" />
|
||||
<dc:Bounds x="1135" y="635" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_1x8azom_di" bpmnElement="Task_EnterEmailMethods">
|
||||
<dc:Bounds x="1850" y="210" width="100" height="80" />
|
||||
<dc:Bounds x="1850" y="480" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_1r640re_di" bpmnElement="Task_EnterDataManagement">
|
||||
<dc:Bounds x="1850" y="350" width="100" height="80" />
|
||||
<dc:Bounds x="1850" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ParallelGateway_0cignbh_di" bpmnElement="ExclusiveGateway_1lpm3pa">
|
||||
<dc:Bounds x="2025" y="365" width="50" height="50" />
|
||||
<dc:Bounds x="2025" y="635" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0ns9m8t_di" bpmnElement="Task_EnterTransmissionMethod">
|
||||
<dc:Bounds x="1850" y="490" width="100" height="80" />
|
||||
<dc:Bounds x="1850" y="760" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="EndEvent_151cj59_di" bpmnElement="EndEvent_151cj59">
|
||||
<dc:Bounds x="2322" y="372" width="36" height="36" />
|
||||
<dc:Bounds x="2322" y="642" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ExclusiveGateway_0pi0c2d_di" bpmnElement="ExclusiveGateway_0pi0c2d" isMarkerVisible="true">
|
||||
<dc:Bounds x="1595" y="365" width="50" height="50" />
|
||||
<dc:Bounds x="1595" y="635" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="1580" y="341" width="80" height="14" />
|
||||
<dc:Bounds x="1580" y="611" width="80" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ParallelGateway_1284xgu_di" bpmnElement="ExclusiveGateway_0x3t2vl">
|
||||
<dc:Bounds x="1725" y="365" width="50" height="50" />
|
||||
<dc:Bounds x="1725" y="635" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ScriptTask_1616pnb_di" bpmnElement="Task_1ypw8ge">
|
||||
<dc:Bounds x="2140" y="350" width="100" height="80" />
|
||||
<dc:Bounds x="2140" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1l6rjbr_di" bpmnElement="Task_0q6ir2l">
|
||||
<dc:Bounds x="251" y="350" width="100" height="80" />
|
||||
<dc:Bounds x="251" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_11u7de2_di" bpmnElement="Task_0uotpzg">
|
||||
<dc:Bounds x="398" y="350" width="100" height="80" />
|
||||
<dc:Bounds x="398" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0nfmn0k_di" bpmnElement="Task_196zozc">
|
||||
<dc:Bounds x="717" y="350" width="100" height="80" />
|
||||
<dc:Bounds x="717" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0q8o038_di" bpmnElement="Task_EnterIndividualUseDevices">
|
||||
<dc:Bounds x="1200" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_10x8kgc_di" bpmnElement="Task_EnterOutsideUVA">
|
||||
<dc:Bounds x="1470" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1qbfs1w_di" bpmnElement="Activity_1qbfs1w">
|
||||
<dc:Bounds x="1330" y="620" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_1f2b80a_di" bpmnElement="Task_EnterUVaServersWebsites">
|
||||
<dc:Bounds x="980" y="770" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0n3jbd7_di" bpmnElement="Task_EnterWebCloudServer">
|
||||
<dc:Bounds x="980" y="870" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Association_0ykpfju_di" bpmnElement="Association_0ykpfju">
|
||||
<di:waypoint x="1080" y="365" />
|
||||
<di:waypoint x="1200" y="306" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Association_1nrg5es_di" bpmnElement="Association_1nrg5es">
|
||||
<di:waypoint x="302" y="620" />
|
||||
<di:waypoint x="306" y="459" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Association_01x19xx_di" bpmnElement="Association_01x19xx">
|
||||
<di:waypoint x="1385" y="700" />
|
||||
<di:waypoint x="1399" y="820" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<decisionTable id="decisionTable_1">
|
||||
<input id="input_1" label="Pharmacy Manual Upload Count">
|
||||
<inputExpression id="inputExpression_1" typeRef="integer">
|
||||
<text>StudyInfo.documents["DrugDevDoc_PharmManual"]["count"]</text>
|
||||
<text>StudyInfo.documents.DrugDevDoc_PharmManual.count</text>
|
||||
</inputExpression>
|
||||
</input>
|
||||
<output id="output_1" label="Pharmacy Manual(s) Uploaded?" name="isPharmacyManual" typeRef="boolean" />
|
||||
|
|
Binary file not shown.
44
deploy.sh
44
deploy.sh
|
@ -1,22 +1,44 @@
|
|||
#!/bin/bash
|
||||
|
||||
function branch_to_tag () {
|
||||
if [ "$1" == "latest" ]; then echo "production"; else echo "$1" ; fi
|
||||
}
|
||||
|
||||
function branch_to_deploy_group() {
|
||||
if [[ $1 =~ ^(rrt\/.*)$ ]]; then echo "rrt"; else echo "crconnect" ; fi
|
||||
}
|
||||
|
||||
function branch_to_deploy_stage () {
|
||||
if [ "$1" == "master" ]; then echo "production"; else echo "$1" ; fi
|
||||
}
|
||||
|
||||
REPO="sartography/cr-connect-workflow"
|
||||
TAG=$(branch_to_tag "$TRAVIS_BRANCH")
|
||||
|
||||
DEPLOY_APP="backend"
|
||||
DEPLOY_GROUP=$(branch_to_deploy_group "$TRAVIS_BRANCH")
|
||||
DEPLOY_STAGE=$(branch_to_deploy_stage "$TRAVIS_BRANCH")
|
||||
|
||||
if [ "$DEPLOY_GROUP" == "rrt" ]; then
|
||||
IFS='/' read -ra ARR <<< "$TRAVIS_BRANCH" # Split branch on '/' character
|
||||
TAG=$(branch_to_tag "rrt_${ARR[1]}")
|
||||
DEPLOY_STAGE=$(branch_to_deploy_stage "${ARR[1]}")
|
||||
fi
|
||||
|
||||
DEPLOY_PATH="$DEPLOY_GROUP/$DEPLOY_STAGE/$DEPLOY_APP"
|
||||
echo "REPO = $REPO"
|
||||
echo "TAG = $TAG"
|
||||
echo "DEPLOY_PATH = $DEPLOY_PATH"
|
||||
|
||||
# Build and push Docker image to Docker Hub
|
||||
echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USERNAME" --password-stdin || exit 1
|
||||
REPO="sartography/cr-connect-workflow"
|
||||
TAG=$(if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo "$TRAVIS_BRANCH" ; fi)
|
||||
COMMIT=${TRAVIS_COMMIT::8}
|
||||
|
||||
docker build -f Dockerfile -t "$REPO:$COMMIT" . || exit 1
|
||||
docker tag "$REPO:$COMMIT" "$REPO:$TAG" || exit 1
|
||||
docker tag "$REPO:$COMMIT" "$REPO:travis-$TRAVIS_BUILD_NUMBER" || exit 1
|
||||
docker build -f Dockerfile -t "$REPO:$TAG" . || exit 1
|
||||
docker push "$REPO" || exit 1
|
||||
|
||||
# Wait for Docker Hub
|
||||
echo "Publishing to Docker Hub..."
|
||||
sleep 30
|
||||
|
||||
# Notify DC/OS that Docker image has been updated
|
||||
# Notify UVA DCOS that Docker image has been updated
|
||||
echo "Refreshing DC/OS..."
|
||||
STAGE=$(if [ "$TRAVIS_BRANCH" == "master" ]; then echo "production"; else echo "$TRAVIS_BRANCH" ; fi)
|
||||
echo "STAGE = $STAGE"
|
||||
aws sqs send-message --region "$AWS_DEFAULT_REGION" --queue-url "$AWS_SQS_URL" --message-body "crconnect/$STAGE/backend" || exit 1
|
||||
aws sqs send-message --region "$AWS_DEFAULT_REGION" --queue-url "$AWS_SQS_URL" --message-body "$DEPLOY_PATH" || exit 1
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import datetime
|
||||
import glob
|
||||
import glob
|
||||
import os
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
|
||||
from crc import app, db, session
|
||||
from crc.models.file import FileType, FileModel, FileDataModel, CONTENT_TYPES
|
||||
from crc.models.study import StudyModel
|
||||
from crc.models.user import UserModel
|
||||
from crc.models.file import CONTENT_TYPES
|
||||
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecCategoryModel
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
from crc.models.protocol_builder import ProtocolBuilderStatus
|
||||
|
||||
|
||||
class ExampleDataLoader:
|
||||
|
@ -19,7 +14,7 @@ class ExampleDataLoader:
|
|||
session.flush() # Clear out any transactions before deleting it all to avoid spurious errors.
|
||||
for table in reversed(db.metadata.sorted_tables):
|
||||
session.execute(table.delete())
|
||||
session.flush()
|
||||
session.flush()
|
||||
|
||||
def load_all(self):
|
||||
|
||||
|
@ -239,7 +234,14 @@ class ExampleDataLoader:
|
|||
def load_reference_documents(self):
|
||||
file_path = os.path.join(app.root_path, 'static', 'reference', 'irb_documents.xlsx')
|
||||
file = open(file_path, "rb")
|
||||
FileService.add_reference_file(FileService.IRB_PRO_CATEGORIES_FILE,
|
||||
FileService.add_reference_file(FileService.DOCUMENT_LIST,
|
||||
binary_data=file.read(),
|
||||
content_type=CONTENT_TYPES['xls'])
|
||||
file.close()
|
||||
|
||||
file_path = os.path.join(app.root_path, 'static', 'reference', 'investigators.xlsx')
|
||||
file = open(file_path, "rb")
|
||||
FileService.add_reference_file(FileService.INVESTIGATOR_LIST,
|
||||
binary_data=file.read(),
|
||||
content_type=CONTENT_TYPES['xls'])
|
||||
file.close()
|
||||
|
|
4
run.py
4
run.py
|
@ -1,3 +1,5 @@
|
|||
from crc import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host='0.0.0.0', port=5000)
|
||||
flask_port = app.config['FLASK_PORT']
|
||||
app.run(host='0.0.0.0', port=flask_port)
|
||||
|
|
|
@ -207,14 +207,12 @@ class BaseTest(unittest.TestCase):
|
|||
study = session.query(StudyModel).first()
|
||||
spec = self.load_test_spec(workflow_name, category_id=category_id)
|
||||
workflow_model = StudyService._create_workflow_model(study, spec)
|
||||
#processor = WorkflowProcessor(workflow_model)
|
||||
#workflow = session.query(WorkflowModel).filter_by(study_id=study.id, workflow_spec_id=workflow_name).first()
|
||||
return workflow_model
|
||||
|
||||
def create_reference_document(self):
|
||||
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'reference', 'irb_documents.xlsx')
|
||||
file_path = os.path.join(app.root_path, 'static', 'reference', 'irb_documents.xlsx')
|
||||
file = open(file_path, "rb")
|
||||
FileService.add_reference_file(FileService.IRB_PRO_CATEGORIES_FILE,
|
||||
FileService.add_reference_file(FileService.DOCUMENT_LIST,
|
||||
binary_data=file.read(),
|
||||
content_type=CONTENT_TYPES['xls'])
|
||||
file.close()
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1j7idla" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1j7idla" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||
<bpmn:process id="Process_18biih5" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>SequenceFlow_1pnq3kg</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="SequenceFlow_1pnq3kg" sourceRef="StartEvent_1" targetRef="Task_Has_Bananas" />
|
||||
<bpmn:userTask id="Task_Has_Bananas" name="Has Bananas?" camunda:formKey="bananas_form">
|
||||
<bpmn:userTask id="Task_Has_Bananas" name="Enter Do You Have Bananas" camunda:formKey="bananas_form">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="has_bananas" label="Do you have bananas?" type="boolean" />
|
||||
|
@ -15,7 +15,7 @@
|
|||
<bpmn:outgoing>SequenceFlow_1lmkn99</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:sequenceFlow id="SequenceFlow_1lmkn99" sourceRef="Task_Has_Bananas" targetRef="ExclusiveGateway_003amsm" />
|
||||
<bpmn:exclusiveGateway id="ExclusiveGateway_003amsm">
|
||||
<bpmn:exclusiveGateway id="ExclusiveGateway_003amsm" name="Has Bananas?">
|
||||
<bpmn:incoming>SequenceFlow_1lmkn99</bpmn:incoming>
|
||||
<bpmn:outgoing>SequenceFlow_Yes_Bananas</bpmn:outgoing>
|
||||
<bpmn:outgoing>SequenceFlow_No_Bananas</bpmn:outgoing>
|
||||
|
@ -55,29 +55,13 @@
|
|||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_18biih5">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1pnq3kg_di" bpmnElement="SequenceFlow_1pnq3kg">
|
||||
<di:waypoint x="215" y="117" />
|
||||
<di:waypoint x="270" y="117" />
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_08djf6q_di" bpmnElement="SequenceFlow_08djf6q">
|
||||
<di:waypoint x="660" y="230" />
|
||||
<di:waypoint x="752" y="230" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="UserTask_0u8fjmw_di" bpmnElement="Task_Has_Bananas">
|
||||
<dc:Bounds x="270" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1lmkn99_di" bpmnElement="SequenceFlow_1lmkn99">
|
||||
<di:waypoint x="370" y="117" />
|
||||
<di:waypoint x="425" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="ExclusiveGateway_14wqqsi_di" bpmnElement="ExclusiveGateway_003amsm" isMarkerVisible="true">
|
||||
<dc:Bounds x="425" y="92" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0f3vx1l_di" bpmnElement="SequenceFlow_Yes_Bananas">
|
||||
<di:waypoint x="475" y="117" />
|
||||
<di:waypoint x="560" y="117" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="509" y="99" width="18" height="40" />
|
||||
</bpmndi:BPMNLabel>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_02z84p5_di" bpmnElement="SequenceFlow_02z84p5">
|
||||
<di:waypoint x="660" y="117" />
|
||||
<di:waypoint x="752" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_12acevn_di" bpmnElement="SequenceFlow_No_Bananas">
|
||||
<di:waypoint x="450" y="142" />
|
||||
|
@ -87,6 +71,33 @@
|
|||
<dc:Bounds x="459" y="183" width="13" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0f3vx1l_di" bpmnElement="SequenceFlow_Yes_Bananas">
|
||||
<di:waypoint x="475" y="117" />
|
||||
<di:waypoint x="560" y="117" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="509" y="99" width="18" height="40" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1lmkn99_di" bpmnElement="SequenceFlow_1lmkn99">
|
||||
<di:waypoint x="370" y="117" />
|
||||
<di:waypoint x="425" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1pnq3kg_di" bpmnElement="SequenceFlow_1pnq3kg">
|
||||
<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="UserTask_0u8fjmw_di" bpmnElement="Task_Has_Bananas">
|
||||
<dc:Bounds x="270" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ExclusiveGateway_14wqqsi_di" bpmnElement="ExclusiveGateway_003amsm" isMarkerVisible="true">
|
||||
<dc:Bounds x="425" y="92" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="415" y="62" width="73" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0ht939a_di" bpmnElement="Task_Num_Bananas">
|
||||
<dc:Bounds x="560" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
|
@ -96,17 +107,9 @@
|
|||
<bpmndi:BPMNShape id="EndEvent_063bpg6_di" bpmnElement="EndEvent_063bpg6">
|
||||
<dc:Bounds x="752" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_02z84p5_di" bpmnElement="SequenceFlow_02z84p5">
|
||||
<di:waypoint x="660" y="117" />
|
||||
<di:waypoint x="752" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="EndEvent_1hwtug4_di" bpmnElement="EndEvent_1hwtug4">
|
||||
<dc:Bounds x="752" y="212" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_08djf6q_di" bpmnElement="SequenceFlow_08djf6q">
|
||||
<di:waypoint x="660" y="230" />
|
||||
<di:waypoint x="752" y="230" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_83c9f25" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||
<bpmn:process id="Process_84bead4" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_1ux3ndu</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1ux3ndu" sourceRef="StartEvent_1" targetRef="Activity_07iglj7" />
|
||||
<bpmn:exclusiveGateway id="Gateway_1lh8c45" name="Decide Which Branch?">
|
||||
<bpmn:incoming>Flow_1ut95vk</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1fok0lz</bpmn:outgoing>
|
||||
<bpmn:outgoing>Flow_01he29w</bpmn:outgoing>
|
||||
</bpmn:exclusiveGateway>
|
||||
<bpmn:sequenceFlow id="Flow_1ut95vk" sourceRef="Activity_07iglj7" targetRef="Gateway_1lh8c45" />
|
||||
<bpmn:sequenceFlow id="Flow_1fok0lz" name="a" sourceRef="Gateway_1lh8c45" targetRef="Activity_19ig0xo">
|
||||
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">which_branch == 'a'</bpmn:conditionExpression>
|
||||
</bpmn:sequenceFlow>
|
||||
<bpmn:sequenceFlow id="Flow_01he29w" name="b" sourceRef="Gateway_1lh8c45" targetRef="Activity_1hx53cu">
|
||||
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">which_branch == 'b'</bpmn:conditionExpression>
|
||||
</bpmn:sequenceFlow>
|
||||
<bpmn:exclusiveGateway id="Gateway_0ikuwt5">
|
||||
<bpmn:incoming>Flow_03ddkww</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_0ozlczo</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1ph05b1</bpmn:outgoing>
|
||||
</bpmn:exclusiveGateway>
|
||||
<bpmn:sequenceFlow id="Flow_03ddkww" sourceRef="Activity_19ig0xo" targetRef="Gateway_0ikuwt5" />
|
||||
<bpmn:sequenceFlow id="Flow_0ozlczo" sourceRef="Activity_1hx53cu" targetRef="Gateway_0ikuwt5" />
|
||||
<bpmn:sequenceFlow id="Flow_1ph05b1" sourceRef="Gateway_0ikuwt5" targetRef="Activity_1b15riu" />
|
||||
<bpmn:userTask id="Activity_07iglj7" name="Enter Task 1" camunda:formKey="form_task_1">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="which_branch" label="Which branch?" type="enum">
|
||||
<camunda:value id="a" name="Task 2a" />
|
||||
<camunda:value id="b" name="Task 2b" />
|
||||
</camunda:formField>
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1ux3ndu</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1ut95vk</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:userTask id="Activity_19ig0xo" name="Enter Task 2a" camunda:formKey="form_task2a">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="FormField_0taj99h" label="What is your favorite color?" type="string" />
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1fok0lz</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_03ddkww</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:userTask id="Activity_1hx53cu" name="Enter Task 2b" camunda:formKey="form_task2b">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="FormField_1l30p68" label="Do you like pie?" type="boolean" />
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_01he29w</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0ozlczo</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:userTask id="Activity_1b15riu" name="Enter Task 3" camunda:formKey="form_task3">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="FormField_3nh4vhj" label="Tell me a bedtime story." type="textarea" />
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1ph05b1</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0kr8pvy</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:endEvent id="Event_0im2hti">
|
||||
<bpmn:incoming>Flow_0kr8pvy</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0kr8pvy" sourceRef="Activity_1b15riu" targetRef="Event_0im2hti" />
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_84bead4">
|
||||
<bpmndi:BPMNEdge id="Flow_0kr8pvy_di" bpmnElement="Flow_0kr8pvy">
|
||||
<di:waypoint x="890" y="177" />
|
||||
<di:waypoint x="952" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ph05b1_di" bpmnElement="Flow_1ph05b1">
|
||||
<di:waypoint x="735" y="177" />
|
||||
<di:waypoint x="790" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ozlczo_di" bpmnElement="Flow_0ozlczo">
|
||||
<di:waypoint x="630" y="290" />
|
||||
<di:waypoint x="710" y="290" />
|
||||
<di:waypoint x="710" y="202" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_03ddkww_di" bpmnElement="Flow_03ddkww">
|
||||
<di:waypoint x="630" y="177" />
|
||||
<di:waypoint x="685" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_01he29w_di" bpmnElement="Flow_01he29w">
|
||||
<di:waypoint x="450" y="202" />
|
||||
<di:waypoint x="450" y="290" />
|
||||
<di:waypoint x="530" y="290" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="462" y="243" width="6" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1fok0lz_di" bpmnElement="Flow_1fok0lz">
|
||||
<di:waypoint x="475" y="177" />
|
||||
<di:waypoint x="530" y="177" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="500" y="159" width="6" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ut95vk_di" bpmnElement="Flow_1ut95vk">
|
||||
<di:waypoint x="370" y="177" />
|
||||
<di:waypoint x="425" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ux3ndu_di" bpmnElement="Flow_1ux3ndu">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="270" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1lh8c45_di" bpmnElement="Gateway_1lh8c45" isMarkerVisible="true">
|
||||
<dc:Bounds x="425" y="152" width="50" height="50" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="417" y="122" width="68" height="27" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0ikuwt5_di" bpmnElement="Gateway_0ikuwt5" isMarkerVisible="true">
|
||||
<dc:Bounds x="685" y="152" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0f7bxmu_di" bpmnElement="Activity_07iglj7">
|
||||
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0pyt443_di" bpmnElement="Activity_19ig0xo">
|
||||
<dc:Bounds x="530" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1mv6e1a_di" bpmnElement="Activity_1hx53cu">
|
||||
<dc:Bounds x="530" y="250" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1hbzn0k_di" bpmnElement="Activity_1b15riu">
|
||||
<dc:Bounds x="790" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_0im2hti_di" bpmnElement="Event_0im2hti">
|
||||
<dc:Bounds x="952" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -74,6 +74,82 @@
|
|||
"Staff"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"cn": [
|
||||
"Dan Funk (dhf8r)"
|
||||
],
|
||||
"displayName": "Dan Funk",
|
||||
"givenName": [
|
||||
"Dan"
|
||||
],
|
||||
"mail": [
|
||||
"dhf8r@virginia.edu"
|
||||
],
|
||||
"objectClass": [
|
||||
"top",
|
||||
"person",
|
||||
"organizationalPerson",
|
||||
"inetOrgPerson",
|
||||
"uvaPerson",
|
||||
"uidObject"
|
||||
],
|
||||
"telephoneNumber": [
|
||||
"+1 (434) 924-1723"
|
||||
],
|
||||
"title": [
|
||||
"E42:He's a hoopy frood"
|
||||
],
|
||||
"uvaDisplayDepartment": [
|
||||
"E0:EN-Eng Study of Parallel Universes"
|
||||
],
|
||||
"uvaPersonIAMAffiliation": [
|
||||
"faculty"
|
||||
],
|
||||
"uvaPersonSponsoredType": [
|
||||
"Staff"
|
||||
]
|
||||
},
|
||||
"dn": "uid=dhf8r,ou=People,o=University of Virginia,c=US",
|
||||
"raw": {
|
||||
"cn": [
|
||||
"Dan Funk (dhf84)"
|
||||
],
|
||||
"displayName": [
|
||||
"Dan Funk"
|
||||
],
|
||||
"givenName": [
|
||||
"Dan"
|
||||
],
|
||||
"mail": [
|
||||
"dhf8r@virginia.edu"
|
||||
],
|
||||
"objectClass": [
|
||||
"top",
|
||||
"person",
|
||||
"organizationalPerson",
|
||||
"inetOrgPerson",
|
||||
"uvaPerson",
|
||||
"uidObject"
|
||||
],
|
||||
"telephoneNumber": [
|
||||
"+1 (434) 924-1723"
|
||||
],
|
||||
"title": [
|
||||
"E42:He's a hoopy frood"
|
||||
],
|
||||
"uvaDisplayDepartment": [
|
||||
"E0:EN-Eng Study of Parallel Universes"
|
||||
],
|
||||
"uvaPersonIAMAffiliation": [
|
||||
"faculty"
|
||||
],
|
||||
"uvaPersonSponsoredType": [
|
||||
"Staff"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||
<bpmn:process id="MultiInstance" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1" name="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_0t6p1sb</bpmn:outgoing>
|
||||
|
@ -17,6 +17,9 @@
|
|||
<camunda:formData>
|
||||
<camunda:formField id="email" label="Email Address:" type="string" />
|
||||
</camunda:formData>
|
||||
<camunda:properties>
|
||||
<camunda:property name="display_name" value="{{investigator.label}}" />
|
||||
</camunda:properties>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>SequenceFlow_1p568pp</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0ugjw69</bpmn:outgoing>
|
||||
|
@ -31,33 +34,33 @@
|
|||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="MultiInstance">
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1p568pp_di" bpmnElement="SequenceFlow_1p568pp">
|
||||
<di:waypoint x="350" y="117" />
|
||||
<di:waypoint x="430" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ugjw69_di" bpmnElement="Flow_0ugjw69">
|
||||
<di:waypoint x="530" y="117" />
|
||||
<di:waypoint x="592" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0t6p1sb_di" bpmnElement="Flow_0t6p1sb">
|
||||
<di:waypoint x="178" y="117" />
|
||||
<di:waypoint x="250" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="142" y="99" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="129" y="142" width="64" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0t6p1sb_di" bpmnElement="Flow_0t6p1sb">
|
||||
<di:waypoint x="178" y="117" />
|
||||
<di:waypoint x="250" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="Event_1g0pmib_di" bpmnElement="Event_End">
|
||||
<dc:Bounds x="592" y="99" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="585" y="142" width="54" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0ugjw69_di" bpmnElement="Flow_0ugjw69">
|
||||
<di:waypoint x="530" y="117" />
|
||||
<di:waypoint x="592" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="Activity_1iyilui_di" bpmnElement="MutiInstanceTask">
|
||||
<dc:Bounds x="430" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1p568pp_di" bpmnElement="SequenceFlow_1p568pp">
|
||||
<di:waypoint x="350" y="117" />
|
||||
<di:waypoint x="430" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="ScriptTask_0cbbirp_di" bpmnElement="Task_1v0e2zu">
|
||||
<dc:Bounds x="250" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
|
|
Binary file not shown.
|
@ -1,12 +1,14 @@
|
|||
import io
|
||||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from crc import session
|
||||
from crc.models.file import FileModel, FileType, FileModelSchema, FileDataModel
|
||||
from crc.models.workflow import WorkflowSpecModel
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
from example_data import ExampleDataLoader
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
|
||||
|
@ -102,7 +104,7 @@ class TestFilesApi(BaseTest):
|
|||
self.assertEqual("application/vnd.ms-excel", file.content_type)
|
||||
|
||||
def test_set_reference_file_bad_extension(self):
|
||||
file_name = FileService.IRB_PRO_CATEGORIES_FILE
|
||||
file_name = FileService.DOCUMENT_LIST
|
||||
data = {'file': (io.BytesIO(b"abcdef"), "does_not_matter.ppt")}
|
||||
rv = self.app.put('/v1.0/reference_file/%s' % file_name, data=data, follow_redirects=True,
|
||||
content_type='multipart/form-data', headers=self.logged_in_headers())
|
||||
|
@ -119,7 +121,9 @@ class TestFilesApi(BaseTest):
|
|||
self.assertEqual(b"abcdef", data_out)
|
||||
|
||||
def test_list_reference_files(self):
|
||||
file_name = FileService.IRB_PRO_CATEGORIES_FILE
|
||||
ExampleDataLoader.clean_db()
|
||||
|
||||
file_name = FileService.DOCUMENT_LIST
|
||||
data = {'file': (io.BytesIO(b"abcdef"), file_name)}
|
||||
rv = self.app.put('/v1.0/reference_file/%s' % file_name, data=data, follow_redirects=True,
|
||||
content_type='multipart/form-data', headers=self.logged_in_headers())
|
||||
|
|
|
@ -24,7 +24,11 @@ class TestStudyDetailsDocumentsScript(BaseTest):
|
|||
convention that we are implementing for the IRB.
|
||||
"""
|
||||
|
||||
def test_validate_returns_error_if_reference_files_do_not_exist(self):
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_validate_returns_error_if_reference_files_do_not_exist(self, mock_get):
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
|
||||
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
study = session.query(StudyModel).first()
|
||||
|
@ -36,7 +40,7 @@ class TestStudyDetailsDocumentsScript(BaseTest):
|
|||
# Remove the reference file.
|
||||
file_model = db.session.query(FileModel). \
|
||||
filter(FileModel.is_reference == True). \
|
||||
filter(FileModel.name == FileService.IRB_PRO_CATEGORIES_FILE).first()
|
||||
filter(FileModel.name == FileService.DOCUMENT_LIST).first()
|
||||
if file_model:
|
||||
db.session.query(FileDataModel).filter(FileDataModel.file_model_id == file_model.id).delete()
|
||||
db.session.query(FileModel).filter(FileModel.id == file_model.id).delete()
|
||||
|
@ -46,7 +50,12 @@ class TestStudyDetailsDocumentsScript(BaseTest):
|
|||
with self.assertRaises(ApiError):
|
||||
StudyInfo().do_task_validate_only(task, study.id, "documents")
|
||||
|
||||
def test_no_validation_error_when_correct_file_exists(self):
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_no_validation_error_when_correct_file_exists(self, mock_get):
|
||||
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
|
||||
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
study = session.query(StudyModel).first()
|
||||
|
@ -58,7 +67,7 @@ class TestStudyDetailsDocumentsScript(BaseTest):
|
|||
|
||||
def test_load_lookup_data(self):
|
||||
self.create_reference_document()
|
||||
dict = FileService.get_file_reference_dictionary()
|
||||
dict = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id'])
|
||||
self.assertIsNotNone(dict)
|
||||
|
||||
def get_required_docs(self):
|
||||
|
|
|
@ -163,3 +163,29 @@ class TestStudyService(BaseTest):
|
|||
# 'workflow_id': 456,
|
||||
# 'workflow_spec_id': 'irb_api_details',
|
||||
# 'status': 'complete',
|
||||
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_docs
|
||||
def test_get_personnel(self, mock_docs):
|
||||
self.load_example_data()
|
||||
|
||||
# mock out the protocol builder
|
||||
docs_response = self.protocol_builder_response('investigators.json')
|
||||
mock_docs.return_value = json.loads(docs_response)
|
||||
|
||||
workflow = self.create_workflow('docx') # The workflow really doesnt matter in this case.
|
||||
investigators = StudyService().get_investigators(workflow.study_id)
|
||||
|
||||
self.assertEquals(9, len(investigators))
|
||||
|
||||
# dhf8r is in the ldap mock data.
|
||||
self.assertEquals("dhf8r", investigators['PI']['user_id'])
|
||||
self.assertEquals("Dan Funk", investigators['PI']['display_name']) # Data from ldap
|
||||
self.assertEquals("Primary Investigator", investigators['PI']['label']) # Data from xls file.
|
||||
self.assertEquals("Always", investigators['PI']['display']) # Data from xls file.
|
||||
|
||||
# asd3v is not in ldap, so an error should be returned.
|
||||
self.assertEquals("asd3v", investigators['DC']['user_id'])
|
||||
self.assertEquals("Unable to locate a user with id asd3v in LDAP", investigators['DC']['error']) # Data from ldap
|
||||
|
||||
# No value is provided for Department Chair
|
||||
self.assertIsNone(investigators['DEPT_CH']['user_id'])
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import os
|
||||
import random
|
||||
from unittest.mock import patch
|
||||
|
||||
from crc import session, app
|
||||
|
@ -65,9 +66,14 @@ class TestTasksApi(BaseTest):
|
|||
self.assertEquals(task_in.title, event.task_title)
|
||||
self.assertEquals(task_in.type, event.task_type)
|
||||
self.assertEquals("COMPLETED", event.task_state)
|
||||
self.assertEquals(task_in.mi_type.value, event.mi_type)
|
||||
self.assertEquals(task_in.mi_count, event.mi_count)
|
||||
self.assertEquals(task_in.mi_index, event.mi_index)
|
||||
# Not sure what vodoo is happening inside of marshmallow to get me in this state.
|
||||
if isinstance(task_in.multi_instance_type, MultiInstanceType):
|
||||
self.assertEquals(task_in.multi_instance_type.value, event.mi_type)
|
||||
else:
|
||||
self.assertEquals(task_in.multi_instance_type, event.mi_type)
|
||||
|
||||
self.assertEquals(task_in.multi_instance_count, event.mi_count)
|
||||
self.assertEquals(task_in.multi_instance_index, event.mi_index)
|
||||
self.assertEquals(task_in.process_name, event.process_name)
|
||||
self.assertIsNotNone(event.date)
|
||||
|
||||
|
@ -81,9 +87,9 @@ class TestTasksApi(BaseTest):
|
|||
workflow = self.create_workflow('random_fact')
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
task = workflow.next_task
|
||||
self.assertEqual("Task_User_Select_Type", task['name'])
|
||||
self.assertEqual(3, len(task['form']["fields"][0]["options"]))
|
||||
self.assertIsNotNone(task['documentation'])
|
||||
self.assertEqual("Task_User_Select_Type", task.name)
|
||||
self.assertEqual(3, len(task.form["fields"][0]["options"]))
|
||||
self.assertIsNotNone(task.documentation)
|
||||
expected_docs = """# h1 Heading 8-)
|
||||
## h2 Heading
|
||||
### h3 Heading
|
||||
|
@ -91,7 +97,7 @@ class TestTasksApi(BaseTest):
|
|||
##### h5 Heading
|
||||
###### h6 Heading
|
||||
"""
|
||||
self.assertTrue(str.startswith(task['documentation'], expected_docs))
|
||||
self.assertTrue(str.startswith(task.documentation, expected_docs))
|
||||
|
||||
def test_two_forms_task(self):
|
||||
# Set up a new workflow
|
||||
|
@ -100,67 +106,78 @@ 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.user_tasks))
|
||||
self.assertIsNotNone(workflow_api.next_task['form'])
|
||||
self.assertEqual("UserTask", workflow_api.next_task['type'])
|
||||
self.assertEqual("StepOne", workflow_api.next_task['name'])
|
||||
self.assertEqual(1, len(workflow_api.next_task['form']['fields']))
|
||||
self.assertEqual(2, 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)
|
||||
self.assertEqual(1, len(workflow_api.next_task.form['fields']))
|
||||
|
||||
# Complete the form for Step one and post it.
|
||||
self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"})
|
||||
self.complete_form(workflow, workflow_api.next_task, {"color": "blue"})
|
||||
|
||||
# Get the next Task
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.assertEqual("StepTwo", workflow_api.next_task['name'])
|
||||
self.assertEqual("StepTwo", workflow_api.next_task.name)
|
||||
|
||||
# Get all user Tasks and check that the data have been saved
|
||||
for task in workflow_api.user_tasks:
|
||||
self.assertIsNotNone(task.data)
|
||||
for val in task.data.values():
|
||||
self.assertIsNotNone(val)
|
||||
task = workflow_api.next_task
|
||||
self.assertIsNotNone(task.data)
|
||||
for val in task.data.values():
|
||||
self.assertIsNotNone(val)
|
||||
|
||||
def test_error_message_on_bad_gateway_expression(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('exclusive_gateway')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.complete_form(workflow, tasks[0], {"has_bananas": True})
|
||||
task = self.get_workflow_api(workflow).next_task
|
||||
self.complete_form(workflow, task, {"has_bananas": True})
|
||||
|
||||
def test_workflow_with_parallel_forms(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('exclusive_gateway')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.complete_form(workflow, tasks[0], {"has_bananas": True})
|
||||
task = self.get_workflow_api(workflow).next_task
|
||||
self.complete_form(workflow, task, {"has_bananas": True})
|
||||
|
||||
# Get the next Task
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.assertEqual("Task_Num_Bananas", workflow_api.next_task['name'])
|
||||
self.assertEqual("Task_Num_Bananas", workflow_api.next_task.name)
|
||||
|
||||
def test_get_workflow_contains_details_about_last_task_data(self):
|
||||
def test_navigation_with_parallel_forms(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('exclusive_gateway')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
workflow_api = self.complete_form(workflow, tasks[0], {"has_bananas": True})
|
||||
|
||||
self.assertIsNotNone(workflow_api.last_task)
|
||||
self.assertEqual({"has_bananas": True}, workflow_api.last_task['data'])
|
||||
|
||||
def test_get_workflow_contains_reference_to_last_task_and_next_task(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('exclusive_gateway')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.complete_form(workflow, tasks[0], {"has_bananas": True})
|
||||
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.assertIsNotNone(workflow_api.last_task)
|
||||
self.assertIsNotNone(workflow_api.next_task)
|
||||
|
||||
self.assertIsNotNone(workflow_api.navigation)
|
||||
nav = workflow_api.navigation
|
||||
self.assertEquals(6, len(nav))
|
||||
self.assertEquals("Do You Have Bananas", nav[0]['title'])
|
||||
self.assertEquals("READY", nav[0]['state'])
|
||||
self.assertEquals("Bananas?", nav[1]['title'])
|
||||
self.assertEquals("FUTURE", nav[1]['state'])
|
||||
self.assertEquals("yes", nav[2]['title'])
|
||||
self.assertEquals("NOOP", nav[2]['state'])
|
||||
|
||||
def test_navigation_with_exclusive_gateway(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('exclusive_gateway_2')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.assertIsNotNone(workflow_api.navigation)
|
||||
nav = workflow_api.navigation
|
||||
self.assertEquals(7, len(nav))
|
||||
self.assertEquals("Task 1", nav[0]['title'])
|
||||
self.assertEquals("Which Branch?", nav[1]['title'])
|
||||
self.assertEquals("a", nav[2]['title'])
|
||||
self.assertEquals("Task 2a", nav[3]['title'])
|
||||
self.assertEquals("b", nav[4]['title'])
|
||||
self.assertEquals("Task 2b", nav[5]['title'])
|
||||
self.assertEquals("Task 3", nav[6]['title'])
|
||||
|
||||
|
||||
def test_document_added_to_workflow_shows_up_in_file_list(self):
|
||||
|
@ -168,7 +185,7 @@ class TestTasksApi(BaseTest):
|
|||
self.create_reference_document()
|
||||
workflow = self.create_workflow('docx')
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
task = self.get_workflow_api(workflow).next_task
|
||||
data = {
|
||||
"full_name": "Buck of the Wild",
|
||||
"date": "5/1/2020",
|
||||
|
@ -176,9 +193,9 @@ class TestTasksApi(BaseTest):
|
|||
"company": "In the company of wolves",
|
||||
"last_name": "Mr. Wolf"
|
||||
}
|
||||
workflow_api = self.complete_form(workflow, tasks[0], data)
|
||||
workflow_api = self.complete_form(workflow, task, data)
|
||||
self.assertIsNotNone(workflow_api.next_task)
|
||||
self.assertEqual("EndEvent_0evb22x", workflow_api.next_task['name'])
|
||||
self.assertEqual("EndEvent_0evb22x", workflow_api.next_task.name)
|
||||
self.assertTrue(workflow_api.status == WorkflowStatus.complete)
|
||||
rv = self.app.get('/v1.0/file?workflow_id=%i' % workflow.id, headers=self.logged_in_headers())
|
||||
self.assert_success(rv)
|
||||
|
@ -196,14 +213,14 @@ class TestTasksApi(BaseTest):
|
|||
workflow = self.create_workflow('random_fact')
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
task = workflow_api.next_task
|
||||
self.assertEqual("Task_User_Select_Type", task['name'])
|
||||
self.assertEqual(3, len(task['form']["fields"][0]["options"]))
|
||||
self.assertIsNotNone(task['documentation'])
|
||||
self.complete_form(workflow, workflow_api.user_tasks[0], {"type": "norris"})
|
||||
self.assertEqual("Task_User_Select_Type", task.name)
|
||||
self.assertEqual(3, len(task.form["fields"][0]["options"]))
|
||||
self.assertIsNotNone(task.documentation)
|
||||
self.complete_form(workflow, workflow_api.next_task, {"type": "norris"})
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.assertEqual("EndEvent_0u1cgrf", workflow_api.next_task['name'])
|
||||
self.assertIsNotNone(workflow_api.next_task['documentation'])
|
||||
self.assertTrue("norris" in workflow_api.next_task['documentation'])
|
||||
self.assertEqual("EndEvent_0u1cgrf", workflow_api.next_task.name)
|
||||
self.assertIsNotNone(workflow_api.next_task.documentation)
|
||||
self.assertTrue("norris" in workflow_api.next_task.documentation)
|
||||
|
||||
def test_load_workflow_from_outdated_spec(self):
|
||||
|
||||
|
@ -211,7 +228,7 @@ class TestTasksApi(BaseTest):
|
|||
self.load_example_data()
|
||||
workflow = self.create_workflow('two_forms')
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"})
|
||||
self.complete_form(workflow, workflow_api.next_task, {"color": "blue"})
|
||||
self.assertTrue(workflow_api.is_latest_spec)
|
||||
|
||||
# Modify the specification, with a major change that alters the flow and can't be deserialized
|
||||
|
@ -238,7 +255,7 @@ class TestTasksApi(BaseTest):
|
|||
self.load_example_data()
|
||||
workflow = self.create_workflow('two_forms')
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.complete_form(workflow, workflow_api.user_tasks[0], {"color": "blue"})
|
||||
self.complete_form(workflow, workflow_api.next_task, {"color": "blue"})
|
||||
self.assertTrue(workflow_api.is_latest_spec)
|
||||
|
||||
# Modify the specification, with a major change that alters the flow and can't be deserialized
|
||||
|
@ -264,23 +281,22 @@ class TestTasksApi(BaseTest):
|
|||
workflow = self.create_workflow('manual_task_with_external_documentation')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
workflow_api = self.complete_form(workflow, tasks[0], {"name": "Dan"})
|
||||
task = self.get_workflow_api(workflow).next_task
|
||||
workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
|
||||
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
self.assertEquals('Task_Manual_One', workflow.next_task['name'])
|
||||
self.assertEquals('ManualTask', workflow_api.next_task['type'])
|
||||
self.assertTrue('Markdown' in workflow_api.next_task['documentation'])
|
||||
self.assertTrue('Dan' in workflow_api.next_task['documentation'])
|
||||
self.assertEquals('Task_Manual_One', workflow.next_task.name)
|
||||
self.assertEquals('ManualTask', workflow_api.next_task.type)
|
||||
self.assertTrue('Markdown' in workflow_api.next_task.documentation)
|
||||
self.assertTrue('Dan' in workflow_api.next_task.documentation)
|
||||
|
||||
def test_bpmn_extension_properties_are_populated(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('manual_task_with_external_documentation')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.assertEquals("JustAKey", tasks[0].properties[0]['id'])
|
||||
self.assertEquals("JustAValue", tasks[0].properties[0]['value'])
|
||||
task = self.get_workflow_api(workflow).next_task
|
||||
self.assertEquals("JustAValue", task.properties['JustAKey'])
|
||||
|
||||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
|
@ -293,11 +309,15 @@ class TestTasksApi(BaseTest):
|
|||
workflow = self.create_workflow('multi_instance')
|
||||
|
||||
# get the first form in the two form workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.assertEquals(1, len(tasks))
|
||||
self.assertEquals("UserTask", tasks[0].type)
|
||||
self.assertEquals(MultiInstanceType.sequential, tasks[0].mi_type)
|
||||
self.assertEquals(3, tasks[0].mi_count)
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
navigation = self.get_workflow_api(workflow).navigation
|
||||
self.assertEquals(4, len(navigation)) # Start task, form_task, multi_task, end task
|
||||
self.assertEquals("UserTask", workflow.next_task.type)
|
||||
self.assertEquals(MultiInstanceType.sequential.value, workflow.next_task.multi_instance_type)
|
||||
self.assertEquals(9, workflow.next_task.multi_instance_count)
|
||||
|
||||
# Assure that the names for each task are properly updated, so they aren't all the same.
|
||||
self.assertEquals("Primary Investigator", workflow.next_task.properties['display_name'])
|
||||
|
||||
|
||||
def test_lookup_endpoint_for_task_field_enumerations(self):
|
||||
|
@ -306,26 +326,30 @@ class TestTasksApi(BaseTest):
|
|||
# get the first form in the two form workflow.
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
task = workflow.next_task
|
||||
field_id = task['form']['fields'][0]['id']
|
||||
field_id = task.form['fields'][0]['id']
|
||||
rv = self.app.get('/v1.0/workflow/%i/task/%s/lookup/%s?query=%s&limit=5' %
|
||||
(workflow.id, task['id'], field_id, 'c'), # All records with a word that starts with 'c'
|
||||
(workflow.id, task.id, field_id, 'c'), # All records with a word that starts with 'c'
|
||||
headers=self.logged_in_headers(),
|
||||
content_type="application/json")
|
||||
self.assert_success(rv)
|
||||
results = json.loads(rv.get_data(as_text=True))
|
||||
self.assertEqual(5, len(results))
|
||||
|
||||
|
||||
def test_sub_process(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('subprocess')
|
||||
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.assertEquals(2, len(tasks))
|
||||
self.assertEquals("UserTask", tasks[0].type)
|
||||
self.assertEquals("Activity_A", tasks[0].name)
|
||||
self.assertEquals("My Sub Process", tasks[0].process_name)
|
||||
workflow_api = self.complete_form(workflow, tasks[0], {"name": "Dan"})
|
||||
task = TaskSchema().load(workflow_api.next_task)
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
navigation = workflow_api.navigation
|
||||
task = workflow_api.next_task
|
||||
|
||||
self.assertEquals(2, len(navigation))
|
||||
self.assertEquals("UserTask", task.type)
|
||||
self.assertEquals("Activity_A", task.name)
|
||||
self.assertEquals("My Sub Process", task.process_name)
|
||||
workflow_api = self.complete_form(workflow, task, {"name": "Dan"})
|
||||
task = workflow_api.next_task
|
||||
self.assertIsNotNone(task)
|
||||
|
||||
self.assertEquals("Activity_B", task.name)
|
||||
|
@ -338,51 +362,47 @@ class TestTasksApi(BaseTest):
|
|||
workflow = self.create_workflow('exclusive_gateway')
|
||||
|
||||
# Start the workflow.
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.complete_form(workflow, tasks[0], {"has_bananas": True})
|
||||
first_task = self.get_workflow_api(workflow).next_task
|
||||
self.complete_form(workflow, first_task, {"has_bananas": True})
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
self.assertEquals('Task_Num_Bananas', workflow.next_task['name'])
|
||||
self.assertEquals('Task_Num_Bananas', workflow.next_task.name)
|
||||
|
||||
# Trying to re-submit the initial task, and answer differently, should result in an error.
|
||||
self.complete_form(workflow, tasks[0], {"has_bananas": False}, error_code="invalid_state")
|
||||
self.complete_form(workflow, first_task, {"has_bananas": False}, error_code="invalid_state")
|
||||
|
||||
# Go ahead and set the number of bananas.
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
task = TaskSchema().load(workflow.next_task)
|
||||
task = workflow.next_task
|
||||
|
||||
self.complete_form(workflow, task, {"num_bananas": 4})
|
||||
# We are now at the end of the workflow.
|
||||
|
||||
# Make the old task the current task.
|
||||
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, tasks[0].id),
|
||||
rv = self.app.put('/v1.0/workflow/%i/task/%s/set_token' % (workflow.id, first_task.id),
|
||||
headers=self.logged_in_headers(),
|
||||
content_type="application/json")
|
||||
self.assert_success(rv)
|
||||
json_data = json.loads(rv.get_data(as_text=True))
|
||||
workflow = WorkflowApiSchema().load(json_data)
|
||||
|
||||
# Assure the last task is the task we were on before the reset,
|
||||
# and the Next Task is the one we just reset the token to be on.
|
||||
self.assertEquals("Task_Has_Bananas", workflow.next_task['name'])
|
||||
self.assertEquals("End", workflow.last_task['name'])
|
||||
# Assure the Next Task is the one we just reset the token to be on.
|
||||
self.assertEquals("Task_Has_Bananas", workflow.next_task.name)
|
||||
|
||||
# Go ahead and get that workflow one more time, it should still be right.
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
|
||||
# Assure the last task is the task we were on before the reset,
|
||||
# and the Next Task is the one we just reset the token to be on.
|
||||
self.assertEquals("Task_Has_Bananas", workflow.next_task['name'])
|
||||
self.assertEquals("End", workflow.last_task['name'])
|
||||
# Assure the Next Task is the one we just reset the token to be on.
|
||||
self.assertEquals("Task_Has_Bananas", workflow.next_task.name)
|
||||
|
||||
# The next task should be a different value.
|
||||
self.complete_form(workflow, tasks[0], {"has_bananas": False})
|
||||
self.complete_form(workflow, workflow.next_task, {"has_bananas": False})
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
self.assertEquals('Task_Why_No_Bananas', workflow.next_task['name'])
|
||||
self.assertEquals('Task_Why_No_Bananas', workflow.next_task.name)
|
||||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_parallel_multi_instance(self, mock_get):
|
||||
|
||||
# Assure we get three investigators back from the API Call, as set in the investigators.json file.
|
||||
# Assure we get nine investigators back from the API Call, as set in the investigators.json file.
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
||||
|
||||
|
@ -390,32 +410,20 @@ class TestTasksApi(BaseTest):
|
|||
self.load_example_data()
|
||||
workflow = self.create_workflow('multi_instance_parallel')
|
||||
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.assertEquals(3, len(tasks))
|
||||
self.assertEquals("UserTask", tasks[0].type)
|
||||
self.assertEquals("MutiInstanceTask", tasks[0].name)
|
||||
self.assertEquals("Gather more information", tasks[0].title)
|
||||
workflow_api = self.get_workflow_api(workflow)
|
||||
self.assertEquals(12, len(workflow_api.navigation))
|
||||
ready_items = [nav for nav in workflow_api.navigation if nav['state'] == "READY"]
|
||||
self.assertEquals(9, len(ready_items))
|
||||
|
||||
self.complete_form(workflow, tasks[0], {"investigator":{"email": "dhf8r@virginia.edu"}})
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
self.assertEquals("UserTask", workflow_api.next_task.type)
|
||||
self.assertEquals("MutiInstanceTask",workflow_api.next_task.name)
|
||||
self.assertEquals("more information", workflow_api.next_task.title)
|
||||
|
||||
self.complete_form(workflow, tasks[2], {"investigator":{"email": "abc@virginia.edu"}})
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
|
||||
self.complete_form(workflow, tasks[1], {"investigator":{"email": "def@virginia.edu"}})
|
||||
tasks = self.get_workflow_api(workflow).user_tasks
|
||||
for i in random.sample(range(9), 9):
|
||||
task = TaskSchema().load(ready_items[i]['task'])
|
||||
self.complete_form(workflow, task, {"investigator":{"email": "dhf8r@virginia.edu"}})
|
||||
#tasks = self.get_workflow_api(workflow).user_tasks
|
||||
|
||||
workflow = self.get_workflow_api(workflow)
|
||||
self.assertEquals(WorkflowStatus.complete, workflow.status)
|
||||
|
||||
# def test_parent_task_set_on_tasks(self):
|
||||
# self.load_example_data()
|
||||
# workflow = self.create_workflow('exclusive_gateway')
|
||||
#
|
||||
# # Start the workflow.
|
||||
# workflow = self.get_workflow_api(workflow)
|
||||
# self.assertEquals(None, workflow.previous_task)
|
||||
# self.complete_form(workflow, workflow.next_task, {"has_bananas": True})
|
||||
# workflow = self.get_workflow_api(workflow)
|
||||
# self.assertEquals('Task_Num_Bananas', workflow.next_task['name'])
|
||||
# self.assertEquals('has_bananas', workflow.previous_task['name'])
|
||||
|
|
|
@ -19,6 +19,7 @@ from crc.services.file_service import FileService
|
|||
from crc.services.study_service import StudyService
|
||||
from crc.models.protocol_builder import ProtocolBuilderStudySchema, ProtocolBuilderInvestigatorSchema, \
|
||||
ProtocolBuilderRequiredDocumentSchema
|
||||
from crc.services.workflow_service import WorkflowService
|
||||
from tests.base_test import BaseTest
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
|
||||
|
@ -26,7 +27,8 @@ from crc.services.workflow_processor import WorkflowProcessor
|
|||
class TestWorkflowProcessor(BaseTest):
|
||||
|
||||
def _populate_form_with_random_data(self, task):
|
||||
WorkflowProcessor.populate_form_with_random_data(task)
|
||||
api_task = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True)
|
||||
WorkflowProcessor.populate_form_with_random_data(task, api_task)
|
||||
|
||||
def get_processor(self, study_model, spec_model):
|
||||
workflow_model = StudyService._create_workflow_model(study_model, spec_model)
|
||||
|
@ -206,7 +208,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
processor.complete_task(next_user_tasks[0])
|
||||
with self.assertRaises(ApiError) as context:
|
||||
processor.do_engine_steps()
|
||||
self.assertEqual("invalid_expression", context.exception.code)
|
||||
self.assertEqual("task_error", context.exception.code)
|
||||
|
||||
def test_workflow_with_docx_template(self):
|
||||
self.load_example_data()
|
||||
|
@ -417,4 +419,4 @@ class TestWorkflowProcessor(BaseTest):
|
|||
task.task_spec.form.fields.append(field)
|
||||
|
||||
with self.assertRaises(ApiError):
|
||||
processor.populate_form_with_random_data(task)
|
||||
self._populate_form_with_random_data(task)
|
|
@ -13,6 +13,23 @@ from tests.base_test import BaseTest
|
|||
class TestWorkflowProcessorMultiInstance(BaseTest):
|
||||
"""Tests the Workflow Processor as it deals with a Multi-Instance task"""
|
||||
|
||||
mock_investigator_response = {'PI': {
|
||||
'label': 'Primary Investigator',
|
||||
'display': 'Always',
|
||||
'unique': 'Yes',
|
||||
'user_id': 'dhf8r',
|
||||
'display_name': 'Dan Funk'},
|
||||
'SC_I': {
|
||||
'label': 'Study Coordinator I',
|
||||
'display': 'Always',
|
||||
'unique': 'Yes',
|
||||
'user_id': None},
|
||||
'DC': {
|
||||
'label': 'Department Contact',
|
||||
'display': 'Optional',
|
||||
'unique': 'Yes',
|
||||
'user_id': 'asd3v',
|
||||
'error': 'Unable to locate a user with id asd3v in LDAP'}}
|
||||
|
||||
def _populate_form_with_random_data(self, task):
|
||||
WorkflowProcessor.populate_form_with_random_data(task)
|
||||
|
@ -21,11 +38,10 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
|
|||
workflow_model = StudyService._create_workflow_model(study_model, spec_model)
|
||||
return WorkflowProcessor(workflow_model)
|
||||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_create_and_complete_workflow(self, mock_get):
|
||||
@patch('crc.services.study_service.StudyService.get_investigators')
|
||||
def test_create_and_complete_workflow(self, mock_study_service):
|
||||
# This depends on getting a list of investigators back from the protocol builder.
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
||||
mock_study_service.return_value = self.mock_investigator_response
|
||||
|
||||
self.load_example_data()
|
||||
workflow_spec_model = self.load_test_spec("multi_instance")
|
||||
|
@ -40,22 +56,14 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
|
|||
|
||||
task = next_user_tasks[0]
|
||||
|
||||
self.assertEquals(
|
||||
{
|
||||
'DC': {'user_id': 'asd3v', 'type_full': 'Department Contact'},
|
||||
'IRBC': {'user_id': 'asdf32', 'type_full': 'IRB Coordinator'},
|
||||
'PI': {'user_id': 'dhf8r', 'type_full': 'Primary Investigator'}
|
||||
},
|
||||
task.data['StudyInfo']['investigators'])
|
||||
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
self.assertEquals("asd3v", task.data["investigator"]["user_id"])
|
||||
self.assertEquals("dhf8r", task.data["investigator"]["user_id"])
|
||||
|
||||
self.assertEqual("MutiInstanceTask", task.get_name())
|
||||
api_task = WorkflowService.spiff_task_to_api_task(task)
|
||||
self.assertEquals(MultiInstanceType.sequential, api_task.mi_type)
|
||||
self.assertEquals(3, api_task.mi_count)
|
||||
self.assertEquals(1, api_task.mi_index)
|
||||
self.assertEquals(MultiInstanceType.sequential, api_task.multi_instance_type)
|
||||
self.assertEquals(3, api_task.multi_instance_count)
|
||||
self.assertEquals(1, api_task.multi_instance_index)
|
||||
task.update_data({"investigator":{"email":"asd3v@virginia.edu"}})
|
||||
processor.complete_task(task)
|
||||
processor.do_engine_steps()
|
||||
|
@ -64,8 +72,8 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
|
|||
api_task = WorkflowService.spiff_task_to_api_task(task)
|
||||
self.assertEqual("MutiInstanceTask", api_task.name)
|
||||
task.update_data({"investigator":{"email":"asdf32@virginia.edu"}})
|
||||
self.assertEquals(3, api_task.mi_count)
|
||||
self.assertEquals(2, api_task.mi_index)
|
||||
self.assertEquals(3, api_task.multi_instance_count)
|
||||
self.assertEquals(2, api_task.multi_instance_index)
|
||||
processor.complete_task(task)
|
||||
processor.do_engine_steps()
|
||||
|
||||
|
@ -73,29 +81,27 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
|
|||
api_task = WorkflowService.spiff_task_to_api_task(task)
|
||||
self.assertEqual("MutiInstanceTask", task.get_name())
|
||||
task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}})
|
||||
self.assertEquals(3, api_task.mi_count)
|
||||
self.assertEquals(3, api_task.mi_index)
|
||||
self.assertEquals(3, api_task.multi_instance_count)
|
||||
self.assertEquals(3, api_task.multi_instance_index)
|
||||
processor.complete_task(task)
|
||||
processor.do_engine_steps()
|
||||
task = processor.bpmn_workflow.last_task
|
||||
|
||||
self.assertEquals(
|
||||
{
|
||||
'DC': {'user_id': 'asd3v', 'type_full': 'Department Contact', 'email': 'asd3v@virginia.edu'},
|
||||
'IRBC': {'user_id': 'asdf32', 'type_full': 'IRB Coordinator', "email": "asdf32@virginia.edu"},
|
||||
'PI': {'user_id': 'dhf8r', 'type_full': 'Primary Investigator', "email": "dhf8r@virginia.edu"}
|
||||
},
|
||||
expected = self.mock_investigator_response
|
||||
expected['PI']['email'] = "asd3v@virginia.edu"
|
||||
expected['SC_I']['email'] = "asdf32@virginia.edu"
|
||||
expected['DC']['email'] = "dhf8r@virginia.edu"
|
||||
self.assertEquals(expected,
|
||||
task.data['StudyInfo']['investigators'])
|
||||
|
||||
self.assertEqual(WorkflowStatus.complete, processor.get_status())
|
||||
|
||||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_create_and_complete_workflow_parallel(self, mock_get):
|
||||
@patch('crc.services.study_service.StudyService.get_investigators')
|
||||
def test_create_and_complete_workflow_parallel(self, mock_study_service):
|
||||
"""Unlike the test above, the parallel task allows us to complete the items in any order."""
|
||||
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
||||
# This depends on getting a list of investigators back from the protocol builder.
|
||||
mock_study_service.return_value = self.mock_investigator_response
|
||||
|
||||
self.load_example_data()
|
||||
workflow_spec_model = self.load_test_spec("multi_instance_parallel")
|
||||
|
@ -110,19 +116,11 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
|
|||
# We can complete the tasks out of order.
|
||||
task = next_user_tasks[2]
|
||||
|
||||
self.assertEquals(
|
||||
{
|
||||
'DC': {'user_id': 'asd3v', 'type_full': 'Department Contact'},
|
||||
'IRBC': {'user_id': 'asdf32', 'type_full': 'IRB Coordinator'},
|
||||
'PI': {'user_id': 'dhf8r', 'type_full': 'Primary Investigator'}
|
||||
},
|
||||
task.data['StudyInfo']['investigators'])
|
||||
|
||||
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
|
||||
self.assertEquals("dhf8r", task.data["investigator"]["user_id"]) # The last of the tasks
|
||||
self.assertEquals("asd3v", task.data["investigator"]["user_id"]) # The last of the tasks
|
||||
|
||||
api_task = WorkflowService.spiff_task_to_api_task(task)
|
||||
self.assertEquals(MultiInstanceType.parallel, api_task.mi_type)
|
||||
self.assertEquals(MultiInstanceType.parallel, api_task.multi_instance_type)
|
||||
task.update_data({"investigator":{"email":"dhf8r@virginia.edu"}})
|
||||
processor.complete_task(task)
|
||||
processor.do_engine_steps()
|
||||
|
@ -142,12 +140,11 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
|
|||
processor.do_engine_steps()
|
||||
|
||||
# Completing the tasks out of order, still provides the correct information.
|
||||
self.assertEquals(
|
||||
{
|
||||
'DC': {'user_id': 'asd3v', 'type_full': 'Department Contact', 'email': 'asd3v@virginia.edu'},
|
||||
'IRBC': {'user_id': 'asdf32', 'type_full': 'IRB Coordinator', "email": "asdf32@virginia.edu"},
|
||||
'PI': {'user_id': 'dhf8r', 'type_full': 'Primary Investigator', "email": "dhf8r@virginia.edu"}
|
||||
},
|
||||
expected = self.mock_investigator_response
|
||||
expected['PI']['email'] = "asd3v@virginia.edu"
|
||||
expected['SC_I']['email'] = "asdf32@virginia.edu"
|
||||
expected['DC']['email'] = "dhf8r@virginia.edu"
|
||||
self.assertEquals(expected,
|
||||
task.data['StudyInfo']['investigators'])
|
||||
|
||||
self.assertEqual(WorkflowStatus.complete, processor.get_status())
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import json
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from crc import session
|
||||
from crc.api.common import ApiErrorSchema
|
||||
from crc.models.file import FileModel
|
||||
from crc.models.protocol_builder import ProtocolBuilderStudySchema
|
||||
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowSpecCategoryModel
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
|
@ -18,7 +20,22 @@ class TestWorkflowSpecValidation(BaseTest):
|
|||
json_data = json.loads(rv.get_data(as_text=True))
|
||||
return ApiErrorSchema(many=True).load(json_data)
|
||||
|
||||
def test_successful_validation_of_test_workflows(self):
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
|
||||
@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
|
||||
def test_successful_validation_of_test_workflows(self, mock_studies, mock_details, mock_docs, mock_investigators):
|
||||
|
||||
# Mock Protocol Builder responses
|
||||
studies_response = self.protocol_builder_response('user_studies.json')
|
||||
mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response)
|
||||
details_response = self.protocol_builder_response('study_details.json')
|
||||
mock_details.return_value = json.loads(details_response)
|
||||
docs_response = self.protocol_builder_response('required_docs.json')
|
||||
mock_docs.return_value = json.loads(docs_response)
|
||||
investigators_response = self.protocol_builder_response('investigators.json')
|
||||
mock_investigators.return_value = json.loads(investigators_response)
|
||||
|
||||
self.assertEqual(0, len(self.validate_workflow("parallel_tasks")))
|
||||
self.assertEqual(0, len(self.validate_workflow("decision_table")))
|
||||
self.assertEqual(0, len(self.validate_workflow("docx")))
|
||||
|
@ -28,7 +45,22 @@ class TestWorkflowSpecValidation(BaseTest):
|
|||
self.assertEqual(0, len(self.validate_workflow("study_details")))
|
||||
self.assertEqual(0, len(self.validate_workflow("two_forms")))
|
||||
|
||||
def test_successful_validation_of_auto_loaded_workflows(self):
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
|
||||
@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
|
||||
def test_successful_validation_of_auto_loaded_workflows(self, mock_studies, mock_details, mock_docs, mock_investigators):
|
||||
|
||||
# Mock Protocol Builder responses
|
||||
studies_response = self.protocol_builder_response('user_studies.json')
|
||||
mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response)
|
||||
details_response = self.protocol_builder_response('study_details.json')
|
||||
mock_details.return_value = json.loads(details_response)
|
||||
docs_response = self.protocol_builder_response('required_docs.json')
|
||||
mock_docs.return_value = json.loads(docs_response)
|
||||
investigators_response = self.protocol_builder_response('investigators.json')
|
||||
mock_investigators.return_value = json.loads(investigators_response)
|
||||
|
||||
self.load_example_data()
|
||||
workflows = session.query(WorkflowSpecModel).all()
|
||||
errors = []
|
||||
|
@ -43,12 +75,12 @@ class TestWorkflowSpecValidation(BaseTest):
|
|||
def test_invalid_expression(self):
|
||||
errors = self.validate_workflow("invalid_expression")
|
||||
self.assertEqual(1, len(errors))
|
||||
self.assertEqual("invalid_expression", errors[0]['code'])
|
||||
self.assertEqual("workflow_execution_exception", errors[0]['code'])
|
||||
self.assertEqual("ExclusiveGateway_003amsm", errors[0]['task_id'])
|
||||
self.assertEqual("Has Bananas Gateway", errors[0]['task_name'])
|
||||
self.assertEqual("invalid_expression.bpmn", errors[0]['file_name'])
|
||||
self.assertEqual("The expression 'this_value_does_not_exist==true' you provided has a missing value."
|
||||
" name 'this_value_does_not_exist' is not defined", errors[0]["message"])
|
||||
self.assertEqual('ExclusiveGateway_003amsm: Error evaluating expression \'this_value_does_not_exist==true\', '
|
||||
'name \'this_value_does_not_exist\' is not defined', errors[0]["message"])
|
||||
|
||||
def test_validation_error(self):
|
||||
errors = self.validate_workflow("invalid_spec")
|
||||
|
|
Loading…
Reference in New Issue