Merge pull request #47 from sartography/testing

Testing
This commit is contained in:
Aaron Louie 2020-05-17 14:00:49 -04:00 committed by GitHub
commit c456b3cdfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1265 additions and 549 deletions

View File

@ -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:

69
Pipfile.lock generated
View File

@ -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": [

View File

@ -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)

View File

@ -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"

View File

@ -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"

View File

@ -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()

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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 {}

View File

@ -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). \

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1wv9t3c" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.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&#10;Details">
<bpmn:incoming>SequenceFlow_0gp2pjm</bpmn:incoming>
<bpmn:outgoing>Flow_0cpwkms</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics />
</bpmn:task>
<bpmn:sequenceFlow id="Flow_0cpwkms" sourceRef="Activity_1qbfs1w" targetRef="Task_EnterOutsideUVA" />
<bpmn:textAnnotation id="TextAnnotation_190dbhy">
<bpmn:text>&gt; Instructions
o Hippa Instructions
o Hippa Indentifiers
o Vuew Definitions and Instructions
o Paper Documents
o Emailed to UVA Personnel
o EMC (EPIC)
o UVA Approvled eCRF
o UVA Servers
o Web or Cloud Server
o Individual Use Devices
o Device Details
0 Outside of UVA
o Outside of UVA?
     o Yes 
           o Email Methods
           o Data Management
           o Transmission Method
           o Generate DSP 
    o No
           o Generate DSP</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1nrg5es" sourceRef="Task_0q6ir2l" targetRef="TextAnnotation_190dbhy" />
<bpmn:textAnnotation id="TextAnnotation_0l6dohi">
<bpmn:text>*  Instructions
* Hippa Instructions
* Hippa Indentifiers
o Vuew Definitions and Instructions
&gt;&gt; Paper Documents
&gt; Emailed to UVA Personnel
&gt; EMC (EPIC)
&gt; UVA Approvled eCRF
&gt; UVA Servers
&gt; Web or Cloud Server
o Individual Use Devices
o Device Details
o Outside of UVA
o Outside of UVA?
     o Yes 
           o Email Methods
           o Data Management
           o Transmission Method
           o Generate DSP 
    o No
           o Generate DSP</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_0ykpfju" sourceRef="Task_EnterPaperDocuments" targetRef="TextAnnotation_0l6dohi" />
<bpmn:textAnnotation id="TextAnnotation_0tpe506">
<bpmn:text>* Instructions
* Hippa Instructions
* Hippa Indentifiers
* View Definitions and Instructions
* Paper Documents (Parallel creates spaces)
* Emailed to UVA Personnel
* EMC (EPIC)
* UVA Approvled eCRF
* UVA Servers
* Web or Cloud Server
* Individual Use Devices
o Device Details (MultiInstance Indents, Parallel creates spaces))
&gt; Desktop
&gt;&gt; Laptop
&gt; Cell Phone
&gt; Other
o Outside of UVA
o Outside of UVA?
     o Yes 
           o Email Methods
           o Data Management
           o Transmission Method
           o Generate DSP 
    o No
           o Generate DSP</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_01x19xx" sourceRef="Activity_1qbfs1w" targetRef="TextAnnotation_0tpe506" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_19ej1y2">
<bpmndi:BPMNShape id="TextAnnotation_0l6dohi_di" bpmnElement="TextAnnotation_0l6dohi">
<dc:Bounds x="1200" y="80" width="283.5273279352227" height="309.04183535762485" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_190dbhy_di" bpmnElement="TextAnnotation_190dbhy">
<dc:Bounds x="190" y="150" width="237.9175101214575" height="309.04183535762485" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_0tpe506_di" bpmnElement="TextAnnotation_0tpe506">
<dc:Bounds x="1240" y="820" width="370" height="442" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_16kyite_di" bpmnElement="SequenceFlow_16kyite">
<di:waypoint x="2240" y="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>

View File

@ -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.

View File

@ -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

View File

@ -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
View File

@ -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)

View File

@ -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()

View File

@ -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>

View File

@ -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>

View File

@ -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"
]
}
}
]
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_17fwemw" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.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>

View File

@ -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())

View File

@ -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):

View File

@ -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'])

View File

@ -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'])

View File

@ -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)

View File

@ -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())

View File

@ -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")