Merge branch 'master' into feature_personnel_multi_instance

This commit is contained in:
Dan Funk 2020-04-15 12:42:36 -04:00
commit 678a45fa76
29 changed files with 567 additions and 292 deletions

8
Pipfile.lock generated
View File

@ -241,11 +241,11 @@
},
"docxtpl": {
"hashes": [
"sha256:216af2580b9f697c2f748faf06c0bfbf47a782f2dd10ad87824a4c5ecbd37008",
"sha256:f5fed6ff724d802f1b151c86ee6141b17cc6fc2fe1979b7840b11db4bd633e48"
"sha256:a46c9cd6ea6d7350a8f16b467c3b1cd09767c83e1da5753f306cc550a7b04959",
"sha256:ab92c5710b6774eff52a90529fb96af29aacfc2d14c0986b6f58ac5bfe403bdf"
],
"index": "pypi",
"version": "==0.8.0"
"version": "==0.9.0"
},
"et-xmlfile": {
"hashes": [
@ -768,7 +768,7 @@
"spiffworkflow": {
"editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "618be41e7e6b20f87865cf9fdd96a79c3cbee065"
"ref": "d46213c9c20859b42ff26c12f852fd32a58d3280"
},
"sqlalchemy": {
"hashes": [

View File

@ -1,15 +1,22 @@
import os
from os import environ
basedir = os.path.abspath(os.path.dirname(__file__))
NAME = "CR Connect Workflow"
CORS_ENABLED = False
DEVELOPMENT = True
SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@localhost:5432/crc_dev"
DEVELOPMENT = environ.get('DEVELOPMENT', default="True")
DB_HOST = environ.get('DB_HOST', default="localhost")
DB_PORT = environ.get('DB_PORT', default="5432")
DB_NAME = environ.get('DB_NAME', default="crc_dev")
DB_USER = environ.get('DB_USER', default="crc_user")
DB_PASSWORD = environ.get('DB_PASSWORD', default="crc_pass")
SQLALCHEMY_DATABASE_URI = environ.get('SQLALCHEMY_DATABASE_URI', default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME))
TOKEN_AUTH_TTL_HOURS = 2
TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod."
FRONTEND_AUTH_CALLBACK = "http://localhost:4200/session"
SWAGGER_AUTH_KEY = "SWAGGER"
TOKEN_AUTH_SECRET_KEY = environ.get('TOKEN_AUTH_SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.")
FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session")
SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER")
#: Default attribute map for single signon.
SSO_ATTRIBUTE_MAP = {
@ -24,7 +31,8 @@ SSO_ATTRIBUTE_MAP = {
}
# %s/%i placeholders expected for uva_id and study_id in various calls.
PB_USER_STUDIES_URL = "http://workflow.sartography.com:5001/pb/user_studies?uva_id=%s"
PB_INVESTIGATORS_URL = "http://workflow.sartography.com:5001/pb/investigators?studyid=%i"
PB_REQUIRED_DOCS_URL = "http://workflow.sartography.com:5001/pb/required_docs?studyid=%i"
PB_STUDY_DETAILS_URL = "http://workflow.sartography.com:5001/pb/study?studyid=%i"
PB_BASE_URL = environ.get('PB_BASE_URL', default="http://localhost:5001/pb/")
PB_USER_STUDIES_URL = environ.get('PB_USER_STUDIES_URL', default=PB_BASE_URL + "user_studies?uva_id=%s")
PB_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + "investigators?studyid=%i")
PB_REQUIRED_DOCS_URL = environ.get('PB_REQUIRED_DOCS_URL', default=PB_BASE_URL + "required_docs?studyid=%i")
PB_STUDY_DETAILS_URL = environ.get('PB_STUDY_DETAILS_URL', default=PB_BASE_URL + "study?studyid=%i")

View File

@ -4,7 +4,7 @@ import connexion
from flask import send_file
from crc import session
from crc.api.common import ApiErrorSchema, ApiError
from crc.api.common import ApiError
from crc.models.file import FileModelSchema, FileModel, FileDataModel
from crc.models.workflow import WorkflowSpecModel
from crc.services.file_service import FileService

View File

@ -10,6 +10,7 @@ from crc.models.stats import WorkflowStatsModel, TaskEventModel
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \
WorkflowSpecCategoryModelSchema
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
def all_specifications():
@ -40,7 +41,7 @@ def validate_workflow_specification(spec_id):
errors = []
try:
WorkflowProcessor.test_spec(spec_id)
WorkflowService.test_spec(spec_id)
except ApiError as ae:
errors.append(ae)
return ApiErrorSchema(many=True).dump(errors)
@ -85,7 +86,7 @@ def delete_workflow_specification(spec_id):
def __get_workflow_api_model(processor: WorkflowProcessor, status_data=None):
spiff_tasks = processor.get_all_user_tasks()
user_tasks = list(map(Task.from_spiff, spiff_tasks))
user_tasks = list(map(WorkflowService.spiff_task_to_api_task, spiff_tasks))
is_active = True
if status_data is not None and processor.workflow_spec_id in status_data:
@ -94,7 +95,7 @@ def __get_workflow_api_model(processor: WorkflowProcessor, status_data=None):
workflow_api = WorkflowApi(
id=processor.get_workflow_id(),
status=processor.get_status(),
last_task=Task.from_spiff(processor.bpmn_workflow.last_task),
last_task=WorkflowService.spiff_task_to_api_task(processor.bpmn_workflow.last_task),
next_task=None,
user_tasks=user_tasks,
workflow_spec_id=processor.workflow_spec_id,
@ -102,7 +103,7 @@ def __get_workflow_api_model(processor: WorkflowProcessor, status_data=None):
is_latest_spec=processor.get_spec_version() == processor.get_latest_version_string(processor.workflow_spec_id),
)
if processor.next_task():
workflow_api.next_task = Task.from_spiff(processor.next_task())
workflow_api.next_task = WorkflowService.spiff_task_to_api_task(processor.next_task())
return workflow_api

View File

@ -1,15 +1,17 @@
import jinja2
import marshmallow
from jinja2 import Template
from marshmallow import INCLUDE
from marshmallow_enum import EnumField
from crc import ma
from crc.api.common import ApiError
from crc.models.workflow import WorkflowStatus
class Task(object):
ENUM_OPTIONS_FILE_PROP = "enum.options.file"
EMUM_OPTIONS_VALUE_COL_PROP = "enum.options.value.column"
EMUM_OPTIONS_LABEL_COL_PROP = "enum.options.label.column"
def __init__(self, id, name, title, type, state, form, documentation, data):
self.id = id
self.name = name
@ -20,35 +22,6 @@ class Task(object):
self.documentation = documentation
self.data = data
@classmethod
def from_spiff(cls, spiff_task):
documentation = spiff_task.task_spec.documentation if hasattr(spiff_task.task_spec, "documentation") else ""
instance = cls(spiff_task.id,
spiff_task.task_spec.name,
spiff_task.task_spec.description,
spiff_task.task_spec.__class__.__name__,
spiff_task.get_state_name(),
None,
documentation,
spiff_task.data)
if hasattr(spiff_task.task_spec, "form"):
instance.form = spiff_task.task_spec.form
if documentation != "" and documentation is not None:
instance.process_documentation(documentation)
return instance
def process_documentation(self, documentation):
'''Runs markdown documentation through the Jinja2 processor to inject data
create loops, etc...'''
try:
template = Template(documentation)
self.documentation = template.render(**self.data)
except jinja2.exceptions.TemplateError as ue:
raise ApiError(code="template_error", message="Error processing template for task %s: %s" %
(self.name, str(ue)), status_code=500)
# TODO: Catch additional errors and report back.
class OptionSchema(ma.Schema):
class Meta:

View File

@ -50,7 +50,6 @@ Takes two arguments:
"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)
workflow_spec_model = self.find_spec_model_in_db(task.workflow)
task_study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
file_name = args[0]
@ -58,21 +57,7 @@ Takes two arguments:
raise ApiError(code="invalid_argument",
message="The given task does not match the given study.")
if workflow_spec_model is None:
raise ApiError(code="workflow_model_error",
message="Something is wrong. I can't find the workflow you are using.")
file_data_model = session.query(FileDataModel) \
.join(FileModel) \
.filter(FileModel.name == file_name) \
.filter(FileModel.workflow_spec_id == workflow_spec_model.id).first()
if file_data_model is None:
raise ApiError(code="file_missing",
message="Can not find a file called '%s' within workflow specification '%s'"
% (args[0], workflow_spec_model.id))
file_data_model = FileService.get_workflow_file_data(task.workflow, file_name)
return self.make_template(BytesIO(file_data_model.data), task.data)
@ -85,15 +70,4 @@ Takes two arguments:
target_stream.seek(0) # move to the beginning of the stream.
return target_stream
def find_spec_model_in_db(self, workflow):
""" Search for the workflow """
# When the workflow spec model is created, we record the primary process id,
# then we can look it up. As there is the potential for sub-workflows, we
# may need to travel up to locate the primary process.
spec = workflow.spec
workflow_model = session.query(WorkflowSpecModel). \
filter(WorkflowSpecModel.primary_process_id == spec.name).first()
if workflow_model is None and workflow != workflow.outer_workflow:
return self.find_spec_model_in_db(workflow.outer_workflow)
return workflow_model

View File

@ -192,3 +192,40 @@ class FileService(object):
if not file_model:
raise ApiError("file_not_found", "There is no reference file with the name '%s'" % file_name)
return FileService.get_file_data(file_model.id, file_model)
@staticmethod
def get_workflow_file_data(workflow, file_name):
"""Given a SPIFF Workflow Model, tracks down a file with the given name in the datbase and returns it's data"""
workflow_spec_model = FileService.__find_spec_model_in_db(workflow)
study_id = workflow.data[WorkflowProcessor.STUDY_ID_KEY]
if workflow_spec_model is None:
raise ApiError(code="workflow_model_error",
message="Something is wrong. I can't find the workflow you are using.")
file_data_model = session.query(FileDataModel) \
.join(FileModel) \
.filter(FileModel.name == file_name) \
.filter(FileModel.workflow_spec_id == workflow_spec_model.id).first()
if file_data_model is None:
raise ApiError(code="file_missing",
message="Can not find a file called '%s' within workflow specification '%s'"
% (file_name, workflow_spec_model.id))
return file_data_model
@staticmethod
def __find_spec_model_in_db(workflow):
""" Search for the workflow """
# When the workflow spec model is created, we record the primary process id,
# then we can look it up. As there is the potential for sub-workflows, we
# may need to travel up to locate the primary process.
spec = workflow.spec
workflow_model = session.query(WorkflowSpecModel). \
filter(WorkflowSpecModel.primary_process_id == spec.name).first()
if workflow_model is None and workflow != workflow.outer_workflow:
return FileService.__find_spec_model_in_db(workflow.outer_workflow)
return workflow_model

View File

@ -17,7 +17,6 @@ from SpiffWorkflow.specs import WorkflowSpec
from crc import session
from crc.api.common import ApiError
from crc.models.api_models import Task
from crc.models.file import FileDataModel, FileModel, FileType
from crc.models.workflow import WorkflowStatus, WorkflowModel
from crc.scripts.script import Script
@ -271,26 +270,7 @@ class WorkflowProcessor(object):
spec.description = version
return spec
@classmethod
def test_spec(cls, spec_id):
spec = WorkflowProcessor.get_spec(spec_id)
bpmn_workflow = BpmnWorkflow(spec, script_engine=cls._script_engine)
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = 1
bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = spec_id
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = True
while not bpmn_workflow.is_completed():
try:
bpmn_workflow.do_engine_steps()
tasks = bpmn_workflow.get_tasks(SpiffTask.READY)
for task in tasks:
task_api = Task.from_spiff(task) # Assure we try to process the documenation, and raise those errors.
WorkflowProcessor.populate_form_with_random_data(task)
task.complete()
except WorkflowException as we:
raise ApiError.from_task_spec("workflow_execution_exception", str(we),
we.sender)
@staticmethod
def populate_form_with_random_data(task):

View File

@ -0,0 +1,112 @@
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from pandas import ExcelFile
from crc.api.common import ApiError
from crc.models.api_models import Task
import jinja2
from jinja2 import Template
from crc.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor, CustomBpmnScriptEngine
from SpiffWorkflow import Task as SpiffTask, WorkflowException
class WorkflowService(object):
"""Provides tools for processing workflows and tasks. This
should at some point, be the only way to work with Workflows, and
the workflow Processor should be hidden behind this service.
This will help maintain a structure that avoids circular dependencies.
But for now, this contains tools for converting spiff-workflow models into our
own API models with additional information and capabilities."""
@classmethod
def test_spec(cls, spec_id):
"""Runs a spec through it's paces to see if it results in any errors. Not full proof, but a good
sanity check."""
spec = WorkflowProcessor.get_spec(spec_id)
bpmn_workflow = BpmnWorkflow(spec, script_engine=CustomBpmnScriptEngine())
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = 1
bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = spec_id
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = True
while not bpmn_workflow.is_completed():
try:
bpmn_workflow.do_engine_steps()
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.complete()
except WorkflowException as we:
raise ApiError.from_task_spec("workflow_execution_exception", str(we),
we.sender)
@staticmethod
def spiff_task_to_api_task(spiff_task):
documentation = spiff_task.task_spec.documentation if hasattr(spiff_task.task_spec, "documentation") else ""
task = Task(spiff_task.id,
spiff_task.task_spec.name,
spiff_task.task_spec.description,
spiff_task.task_spec.__class__.__name__,
spiff_task.get_state_name(),
None,
documentation,
spiff_task.data)
# Only process the form and documentation if this is something that is ready or completed.
if not (spiff_task._is_predicted()):
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)
if documentation != "" and documentation is not None:
WorkflowService._process_documentation(task, documentation)
return task
@staticmethod
def _process_documentation(task, documentation):
"""Runs the given documentation string through the Jinja2 processor to inject data
create loops, etc..."""
try:
template = Template(documentation)
task.documentation = template.render(**task.data)
except jinja2.exceptions.TemplateError as ue:
raise ApiError(code="template_error", message="Error processing template for task %s: %s" %
(task.name, str(ue)), status_code=500)
# TODO: Catch additional errors and report back.
@staticmethod
def _process_options(spiff_task, field):
""" Checks to see if the options are provided in a separate lookup table associated with the
workflow, and populates these if possible. """
if field.has_property(Task.ENUM_OPTIONS_FILE_PROP):
if not field.has_property(Task.EMUM_OPTIONS_VALUE_COL_PROP) or \
not field.has_property(Task.EMUM_OPTIONS_LABEL_COL_PROP):
raise ApiError.from_task("invalid_emum",
"For emumerations based on an xls file, you must include 3 properties: %s, "
"%s, and %s, you supplied %s" % (Task.ENUM_OPTIONS_FILE_PROP,
Task.EMUM_OPTIONS_VALUE_COL_PROP,
Task.EMUM_OPTIONS_LABEL_COL_PROP),
task=spiff_task)
# Get the file data from the File Service
file_name = field.get_property(Task.ENUM_OPTIONS_FILE_PROP)
value_column = field.get_property(Task.EMUM_OPTIONS_VALUE_COL_PROP)
label_column = field.get_property(Task.EMUM_OPTIONS_LABEL_COL_PROP)
data_model = FileService.get_workflow_file_data(spiff_task.workflow, file_name)
xls = ExcelFile(data_model.data)
df = xls.parse(xls.sheet_names[0])
if value_column not in df:
raise ApiError("invalid_emum",
"The file %s does not contain a column named % s" % (file_name, value_column))
if label_column not in df:
raise ApiError("invalid_emum",
"The file %s does not contain a column named % s" % (file_name, label_column))
for index, row in df.iterrows():
field.options.append({"id": row[value_column],
"name": row[label_column]})

View File

@ -14,13 +14,13 @@
</camunda:formField>
<camunda:formField id="FormField_BudgetDraft" label="Draft Budget" type="file">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.FormField_isBudget" />
<camunda:property id="hide_expression" value="!(model.FormField_isBudget) | FormField_isBudget == null" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="FormField_Budget Final" label="Final Budget" type="file">
<camunda:properties>
<camunda:property id="description" value="This is the budget that you will negotiate with your funding source." />
<camunda:property id="hide_expression" value="!model.FormField_isBudget" />
<camunda:property id="hide_expression" value="!(model.FormField_isBudget) | FormField_isBudget == null" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
@ -54,7 +54,7 @@
<bpmn:sequenceFlow id="SequenceFlow_157c6e9" sourceRef="ExclusiveGateway_0m1n8mu" targetRef="Task_0xn3d6z" />
<bpmn:sequenceFlow id="SequenceFlow_1oh6eq7" sourceRef="ExclusiveGateway_0m1n8mu" targetRef="Task_0dj66yz" />
<bpmn:userTask id="Task_0dj66yz" name="Enter Contract Funded" camunda:formKey="FormKey_ContractFunded">
<bpmn:documentation>#### Process:
<bpmn:documentation>#### Process:
The study team uploads the executed copy of the contract(s) after they receive it from the Office of Grants and Contracts, after the following process components are completed outside of the Clinical Research Connect:
@ -111,7 +111,7 @@ If you have any questions about the process, contact contract negotiator or Offi
<bpmn:documentation>#### Non-Funded Executed Agreement
#### Process:
#### Process:
OGC will upload the Non-Funded Executed Agreement after it has been negotiated by OSP contract negotiator.</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1">
<input id="input_1" label="Investigator&#39;s Brochure Form Upload Count">
<inputExpression id="inputExpression_1" typeRef="integer">
<text>Documents["DrugDevDoc_InvestBrochure"]["count"]</text>
<text>Documents.DrugDevDoc_InvestBrochure.count</text>
</inputExpression>
</input>
<output id="output_1" label="Investigator&#39;s Brochure(s) Uploaded?" name="isInvestigatorsBrochure" typeRef="boolean" />

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1">
<input id="input_1" label="IVRS-IWRS-IXRS Manual Count">
<inputExpression id="inputExpression_1" typeRef="integer">
<text>Documents["DrugDevDoc_IVRSIWRSIXRSMan"]["count"]</text>
<text>Documents.DrugDevDoc_IVRSIWRSIXRSMan.count</text>
</inputExpression>
</input>
<output id="output_1" label="IVRS-IWRS-IXRS Manual Uploaded?" name="isIVRS-IWRS-IXRS" typeRef="boolean" />

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_65a4c07" name="DRD" namespace="http://camunda.org/schema/1.0/dmn">
<decision id="Decision_Coordinator" name="Coordinator Status">
<extensionElements>
<biodi:bounds x="200" y="140" width="180" height="80" />
</extensionElements>
<decisionTable id="decisionTable_1">
<input id="input_1" label="Coordinator Status">
<inputExpression id="inputExpression_1" typeRef="boolean">
<text></text>
</inputExpression>
</input>
<output id="output_1" label="Coordinator Form Banner" name="ElementDoc_Coordinator" typeRef="string" />
<rule id="DecisionRule_0sfkgkh">
<inputEntry id="UnaryTests_160rjry">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1lxmr3n">
<text>"Placeholder"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_305b1db" name="DRD" namespace="http://camunda.org/schema/1.0/dmn">
<decision id="Decision_DepartmentChair" name="Department Chair Status">
<extensionElements>
<biodi:bounds x="200" y="150" width="180" height="80" />
</extensionElements>
<decisionTable id="decisionTable_1">
<input id="input_1" label="Department Chair Status">
<inputExpression id="inputExpression_1" typeRef="boolean">
<text></text>
</inputExpression>
</input>
<output id="output_1" label="Department Chair Form Banner" name="ElementDoc_DepartmentChair" typeRef="string" />
<rule id="DecisionRule_1by29jj">
<inputEntry id="UnaryTests_1gr92h8">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_0xadafz">
<text>"DC Placeholder"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -10,14 +10,7 @@
<bpmn:script>StudyInfo investigators</bpmn:script>
</bpmn:scriptTask>
<bpmn:userTask id="UserTask_EditPrimaryInvestigator" name="Edit Primary Investigator" camunda:formKey="PrimaryInvestigator">
<bpmn:documentation>### From Protocol Builder
{% for personnel in study.investigators|selectattr("INVESTIGATORTYPE", "equalto", "PI") %}
#### {{ personnel.INVESTIGATORTYPEFULL }}
{{ personnel.NETBADGEID }}
{% else %}
#### No Primary Investigator Entered in Protocol Builder
The PI is needed for many required steps. Please enter this information in Protocol Builder as soon as possible.
{% endfor %}</bpmn:documentation>
<bpmn:documentation>{{ElementDoc_PrimaryInvestigator}}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="PI_Experience" label="Investigator&#39;s Experience" type="textarea">
@ -44,7 +37,7 @@ The PI is needed for many required steps. Please enter this information in Prot
<bpmn:sequenceFlow id="SequenceFlow_122pp0f" sourceRef="UserTask_EditPrimaryInvestigator" targetRef="Gateway_19lvya6" />
<bpmn:sequenceFlow id="Flow_0g0o593" sourceRef="Gateway_13kno0x" targetRef="UserTask_EditPrimaryInvestigator" />
<bpmn:parallelGateway id="Gateway_13kno0x">
<bpmn:incoming>Flow_05aywbq</bpmn:incoming>
<bpmn:incoming>Flow_19i1d30</bpmn:incoming>
<bpmn:outgoing>Flow_0g0o593</bpmn:outgoing>
<bpmn:outgoing>Flow_1nudg96</bpmn:outgoing>
<bpmn:outgoing>Flow_18ix81l</bpmn:outgoing>
@ -58,13 +51,6 @@ The PI is needed for many required steps. Please enter this information in Prot
</bpmn:parallelGateway>
<bpmn:sequenceFlow id="Flow_1nudg96" sourceRef="Gateway_13kno0x" targetRef="Activity_EditCoordinator" />
<bpmn:userTask id="Activity_EditCoordinator" name="Edit Coordinator" camunda:formKey="Coordinator">
<bpmn:documentation>### From Protocol Builder
{% for personnel in study.investigators|selectattr("INVESTIGATORTYPE", "equalto", "SC_I") %}
#### {{ personnel.INVESTIGATORTYPEFULL }}
{{ personnel.NETBADGEID }}
{% else %}
#### No Primary Coordinator Entered in Protocol Builder
{% endfor %}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="CoordinatorI_EditAccess" label="Should this Coordinator have Study Team editing access in the system?" type="boolean" defaultValue="true" />
@ -77,13 +63,6 @@ The PI is needed for many required steps. Please enter this information in Prot
<bpmn:sequenceFlow id="Flow_00lv37g" sourceRef="Activity_EditCoordinator" targetRef="Gateway_19lvya6" />
<bpmn:sequenceFlow id="Flow_18ix81l" sourceRef="Gateway_13kno0x" targetRef="Activity_EditDepartmentChair" />
<bpmn:userTask id="Activity_EditDepartmentChair" name="Edit Department Chair" camunda:formKey="DeptmentChair">
<bpmn:documentation>### From Protocol Builder
{% for personnel in study.investigators|selectattr("INVESTIGATORTYPE", "equalto", "DEPT_CH") %}
#### {{ personnel.INVESTIGATORTYPEFULL }}
{{ personnel.NETBADGEID }}
{% else %}
#### No Department Chair Entered in Protocol Builder
{% endfor %}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="DepartmentChair_EditAccess" label="Should the Department Chair have Study Team editing access in the system?" type="boolean" defaultValue="false" />
@ -94,74 +73,110 @@ The PI is needed for many required steps. Please enter this information in Prot
<bpmn:outgoing>Flow_0y1jvdw</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0y1jvdw" sourceRef="Activity_EditDepartmentChair" targetRef="Gateway_19lvya6" />
<bpmn:sequenceFlow id="Flow_05aywbq" sourceRef="ScriptTask_LoadPersonnel" targetRef="Gateway_13kno0x" />
<bpmn:sequenceFlow id="Flow_05aywbq" sourceRef="ScriptTask_LoadPersonnel" targetRef="Activity_PI-Satus" />
<bpmn:sequenceFlow id="Flow_0kcrx5l" sourceRef="StartEvent_1" targetRef="ScriptTask_LoadPersonnel" />
<bpmn:sequenceFlow id="Flow_12rh5aj" sourceRef="Activity_PI-Satus" targetRef="Activity_CoordinatorStatus" />
<bpmn:businessRuleTask id="Activity_PI-Satus" name="Primary Investigator Status" camunda:decisionRef="Decision_PrimaryInvestigator">
<bpmn:incoming>Flow_05aywbq</bpmn:incoming>
<bpmn:outgoing>Flow_12rh5aj</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:sequenceFlow id="Flow_04nzqn8" sourceRef="Activity_CoordinatorStatus" targetRef="Activity_DepartmentChairStatus" />
<bpmn:businessRuleTask id="Activity_CoordinatorStatus" name="Coordinator Status" camunda:decisionRef="Decision_Coordinator">
<bpmn:incoming>Flow_12rh5aj</bpmn:incoming>
<bpmn:outgoing>Flow_04nzqn8</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:sequenceFlow id="Flow_19i1d30" sourceRef="Activity_DepartmentChairStatus" targetRef="Gateway_13kno0x" />
<bpmn:businessRuleTask id="Activity_DepartmentChairStatus" name="Department Chair Status" camunda:decisionRef="Decision_DepartmentChair">
<bpmn:incoming>Flow_04nzqn8</bpmn:incoming>
<bpmn:outgoing>Flow_19i1d30</bpmn:outgoing>
</bpmn:businessRuleTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_01143nb">
<bpmndi:BPMNEdge id="Flow_19i1d30_di" bpmnElement="Flow_19i1d30">
<di:waypoint x="580" y="170" />
<di:waypoint x="645" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_04nzqn8_di" bpmnElement="Flow_04nzqn8">
<di:waypoint x="440" y="170" />
<di:waypoint x="480" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_12rh5aj_di" bpmnElement="Flow_12rh5aj">
<di:waypoint x="290" y="170" />
<di:waypoint x="340" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0kcrx5l_di" bpmnElement="Flow_0kcrx5l">
<di:waypoint x="168" y="170" />
<di:waypoint x="270" y="170" />
<di:waypoint x="-52" y="170" />
<di:waypoint x="30" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_05aywbq_di" bpmnElement="Flow_05aywbq">
<di:waypoint x="370" y="170" />
<di:waypoint x="475" y="170" />
<di:waypoint x="130" y="170" />
<di:waypoint x="190" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0y1jvdw_di" bpmnElement="Flow_0y1jvdw">
<di:waypoint x="710" y="300" />
<di:waypoint x="810" y="300" />
<di:waypoint x="810" y="195" />
<di:waypoint x="880" y="300" />
<di:waypoint x="980" y="300" />
<di:waypoint x="980" y="195" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18ix81l_di" bpmnElement="Flow_18ix81l">
<di:waypoint x="500" y="195" />
<di:waypoint x="500" y="300" />
<di:waypoint x="610" y="300" />
<di:waypoint x="670" y="195" />
<di:waypoint x="670" y="300" />
<di:waypoint x="780" y="300" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_00lv37g_di" bpmnElement="Flow_00lv37g">
<di:waypoint x="710" y="170" />
<di:waypoint x="785" y="170" />
<di:waypoint x="880" y="170" />
<di:waypoint x="955" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1nudg96_di" bpmnElement="Flow_1nudg96">
<di:waypoint x="525" y="170" />
<di:waypoint x="610" y="170" />
<di:waypoint x="695" y="170" />
<di:waypoint x="780" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_14469u8_di" bpmnElement="Flow_14469u8">
<di:waypoint x="835" y="170" />
<di:waypoint x="932" y="170" />
<di:waypoint x="1005" y="170" />
<di:waypoint x="1102" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0g0o593_di" bpmnElement="Flow_0g0o593">
<di:waypoint x="500" y="145" />
<di:waypoint x="500" y="30" />
<di:waypoint x="610" y="30" />
<di:waypoint x="670" y="145" />
<di:waypoint x="670" y="30" />
<di:waypoint x="780" y="30" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_122pp0f_di" bpmnElement="SequenceFlow_122pp0f">
<di:waypoint x="710" y="30" />
<di:waypoint x="810" y="30" />
<di:waypoint x="810" y="145" />
<di:waypoint x="880" y="30" />
<di:waypoint x="980" y="30" />
<di:waypoint x="980" y="145" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="132" y="152" width="36" height="36" />
<dc:Bounds x="-88" y="152" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_0h49cmf_di" bpmnElement="ScriptTask_LoadPersonnel">
<dc:Bounds x="270" y="130" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1qor16n_di" bpmnElement="EndEvent_1qor16n">
<dc:Bounds x="932" y="152" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1lvmp6i_di" bpmnElement="Gateway_13kno0x">
<dc:Bounds x="475" y="145" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0k9iptd_di" bpmnElement="Gateway_19lvya6">
<dc:Bounds x="785" y="145" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0d622qi_di" bpmnElement="Activity_EditDepartmentChair">
<dc:Bounds x="610" y="260" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0fl86y3_di" bpmnElement="Activity_EditCoordinator">
<dc:Bounds x="610" y="130" width="100" height="80" />
<dc:Bounds x="30" y="130" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0hpnrm9_di" bpmnElement="UserTask_EditPrimaryInvestigator">
<dc:Bounds x="610" y="-10" width="100" height="80" />
<dc:Bounds x="780" y="-10" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1qor16n_di" bpmnElement="EndEvent_1qor16n">
<dc:Bounds x="1102" y="152" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1lvmp6i_di" bpmnElement="Gateway_13kno0x">
<dc:Bounds x="645" y="145" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0k9iptd_di" bpmnElement="Gateway_19lvya6">
<dc:Bounds x="955" y="145" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0fl86y3_di" bpmnElement="Activity_EditCoordinator">
<dc:Bounds x="780" y="130" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0d622qi_di" bpmnElement="Activity_EditDepartmentChair">
<dc:Bounds x="780" y="260" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1yggezg_di" bpmnElement="Activity_PI-Satus">
<dc:Bounds x="190" y="130" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_10gqxcp_di" bpmnElement="Activity_CoordinatorStatus">
<dc:Bounds x="340" y="130" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1g0yuo4_di" bpmnElement="Activity_DepartmentChairStatus">
<dc:Bounds x="480" y="130" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_cc48f13" name="DRD" namespace="http://camunda.org/schema/1.0/dmn">
<decision id="Decision_PrimaryInvestigator" name="Primary Investigator Status">
<extensionElements>
<biodi:bounds x="200" y="160" width="180" height="80" />
</extensionElements>
<decisionTable id="decisionTable_1">
<input id="input_1" label="Primary Investogator Status">
<inputExpression id="inputExpression_1" typeRef="boolean" expressionLanguage="feel">
<text>list contains( for i in [study.investigators[0].INVESTIGATORTYPE, study.investigators[1].INVESTIGATORTYPE, study.investigators[2].INVESTIGATORTYPE] return i, "PI")</text>
</inputExpression>
</input>
<output id="output_1" label="Primary Investigator Form Banner" name="ElementDoc_PrimaryInvestigator" typeRef="string" />
<rule id="DecisionRule_19gl4re">
<inputEntry id="UnaryTests_14311bk">
<text>true</text>
</inputEntry>
<outputEntry id="LiteralExpression_1d3eboq">
<text>"Placeholder - True"</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1crmfau">
<inputEntry id="UnaryTests_110jbd6">
<text>false</text>
</inputEntry>
<outputEntry id="LiteralExpression_107i08h">
<text>"Placeholder - False"</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -2,12 +2,12 @@
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_1p34ouw" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<decision id="data_security_plan" name="Data Security Plan">
<extensionElements>
<biodi:bounds x="190" y="80" width="180" height="80" />
<biodi:bounds x="180" y="80" width="180" height="80" />
</extensionElements>
<decisionTable id="DecisionTable_1mjqwlv">
<input id="InputClause_18pwfqu" label="Data Plan Required in PB?">
<input id="InputClause_18pwfqu" label="Required Doc Keys">
<inputExpression id="LiteralExpression_1y84stb" typeRef="boolean" expressionLanguage="feel">
<text>Documents['Study_DataSecurityPlan']['required']</text>
<text>Documents['UVACompl_PRCAppr']['required']</text>
</inputExpression>
</input>
<output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" />

View File

@ -2,7 +2,7 @@
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_1p34ouw" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<decision id="enter_core_info" name="Enter Core Info">
<extensionElements>
<biodi:bounds x="170" y="60" width="180" height="80" />
<biodi:bounds x="160" y="60" width="180" height="80" />
</extensionElements>
<decisionTable id="decisionTable_1">
<input id="InputClause_1ki80j6" label="required doc ids">

View File

@ -2,10 +2,10 @@
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/1.0" id="Definitions_1p34ouw" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<decision id="sponsor_funding_source" name="Sponsor Funding Source">
<extensionElements>
<biodi:bounds x="190" y="70" width="180" height="80" />
<biodi:bounds x="190" y="80" width="180" height="80" />
</extensionElements>
<decisionTable id="DecisionTable_00zdxg0">
<input id="InputClause_02n3ccs" label="Sponsor Document Required in PB?">
<input id="InputClause_02n3ccs" label="CoCApplication Required?">
<inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel">
<text>Documents['AD_LabManual']['required']</text>
</inputExpression>

View File

@ -60,13 +60,52 @@
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0jhpidf">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="192" y="421" width="36" height="36" />
<bpmndi:BPMNShape id="TextAnnotation_0ydnva4_di" bpmnElement="TextAnnotation_0ydnva4">
<dc:Bounds x="155" y="210" width="110" height="82" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0x9580l_di" bpmnElement="Flow_0x9580l">
<di:waypoint x="740" y="550" />
<di:waypoint x="800" y="550" />
<di:waypoint x="800" y="464" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1txrak2_di" bpmnElement="Flow_1txrak2">
<di:waypoint x="740" y="439" />
<di:waypoint x="775" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1nimppb_di" bpmnElement="Flow_1nimppb">
<di:waypoint x="608" y="439" />
<di:waypoint x="640" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18pl92p_di" bpmnElement="Flow_18pl92p">
<di:waypoint x="583" y="464" />
<di:waypoint x="583" y="550" />
<di:waypoint x="640" y="550" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_17ct47v_di" bpmnElement="SequenceFlow_17ct47v">
<di:waypoint x="400" y="439" />
<di:waypoint x="558" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1m8285h_di" bpmnElement="Flow_1m8285h">
<di:waypoint x="583" y="414" />
<di:waypoint x="583" y="330" />
<di:waypoint x="640" y="330" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0pwtiqm_di" bpmnElement="Flow_0pwtiqm">
<di:waypoint x="825" y="439" />
<di:waypoint x="862" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1sggkit_di" bpmnElement="Flow_1sggkit">
<di:waypoint x="740" y="330" />
<di:waypoint x="800" y="330" />
<di:waypoint x="800" y="414" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1ees8ka_di" bpmnElement="SequenceFlow_1ees8ka">
<di:waypoint x="228" y="439" />
<di:waypoint x="300" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="192" y="421" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_135x8jg_di" bpmnElement="Event_135x8jg">
<dc:Bounds x="862" y="421" width="36" height="36" />
</bpmndi:BPMNShape>
@ -76,74 +115,35 @@
<bpmndi:BPMNShape id="Activity_1yqy50i_di" bpmnElement="Activity_1yqy50i">
<dc:Bounds x="640" y="290" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1kk6x70_di" bpmnElement="Gateway_12tpgcy">
<dc:Bounds x="775" y="414" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1m22g4p_di" bpmnElement="Gateway_1nta7st">
<dc:Bounds x="558" y="414" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1k5eeun_di" bpmnElement="Activity_1k5eeun">
<dc:Bounds x="640" y="399" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_16cm213_di" bpmnElement="Activity_16cm213">
<dc:Bounds x="640" y="510" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1pv8ygy_di" bpmnElement="TextAnnotation_1pv8ygy">
<dc:Bounds x="300" y="247" width="100" height="68" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1f52jro_di" bpmnElement="TextAnnotation_1f52jro">
<dc:Bounds x="461" y="80" width="243" height="124" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_0w69z3w_di" bpmnElement="Association_0w69z3w">
<di:waypoint x="350" y="399" />
<di:waypoint x="350" y="315" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TextAnnotation_0ydnva4_di" bpmnElement="TextAnnotation_0ydnva4">
<dc:Bounds x="155" y="220" width="110" height="82" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_0a41ixa_di" bpmnElement="Association_0a41ixa">
<di:waypoint x="210" y="421" />
<di:waypoint x="210" y="302" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TextAnnotation_1f52jro_di" bpmnElement="TextAnnotation_1f52jro">
<dc:Bounds x="461" y="80" width="243" height="124" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1sggkit_di" bpmnElement="Flow_1sggkit">
<di:waypoint x="740" y="330" />
<di:waypoint x="800" y="330" />
<di:waypoint x="800" y="414" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Gateway_1kk6x70_di" bpmnElement="Gateway_12tpgcy">
<dc:Bounds x="775" y="414" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0pwtiqm_di" bpmnElement="Flow_0pwtiqm">
<di:waypoint x="825" y="439" />
<di:waypoint x="862" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Gateway_1m22g4p_di" bpmnElement="Gateway_1nta7st">
<dc:Bounds x="558" y="414" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1mzqzwj_di" bpmnElement="Association_1mzqzwj">
<di:waypoint x="583" y="414" />
<di:waypoint x="583" y="204" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1m8285h_di" bpmnElement="Flow_1m8285h">
<di:waypoint x="583" y="414" />
<di:waypoint x="583" y="330" />
<di:waypoint x="640" y="330" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_17ct47v_di" bpmnElement="SequenceFlow_17ct47v">
<di:waypoint x="400" y="439" />
<di:waypoint x="558" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18pl92p_di" bpmnElement="Flow_18pl92p">
<di:waypoint x="583" y="464" />
<di:waypoint x="583" y="550" />
<di:waypoint x="640" y="550" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1nimppb_di" bpmnElement="Flow_1nimppb">
<di:waypoint x="608" y="439" />
<di:waypoint x="640" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_1k5eeun_di" bpmnElement="Activity_1k5eeun">
<dc:Bounds x="640" y="399" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1txrak2_di" bpmnElement="Flow_1txrak2">
<di:waypoint x="740" y="439" />
<di:waypoint x="775" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_16cm213_di" bpmnElement="Activity_16cm213">
<dc:Bounds x="640" y="510" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0x9580l_di" bpmnElement="Flow_0x9580l">
<di:waypoint x="740" y="550" />
<di:waypoint x="800" y="550" />
<di:waypoint x="800" y="464" />
<bpmndi:BPMNEdge id="Association_0a41ixa_di" bpmnElement="Association_0a41ixa">
<di:waypoint x="210" y="421" />
<di:waypoint x="210" y="292" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

@ -65,13 +65,6 @@ class ExampleDataLoader:
]
db.session.add_all(categories)
db.session.commit()
self.create_spec(id="top_level_workflow",
name="top_level_workflow",
display_name="Top Level Workflow",
description="Determines the status of other workflows in a study",
category_id=None,
master_spec=True
)
# Pass IRB Review
self.create_spec(id="irb_api_personnel",
@ -163,6 +156,14 @@ class ExampleDataLoader:
category_id=5,
display_order=0)
# Top Level (Master Status) Workflow
self.create_spec(id="top_level_workflow",
name="top_level_workflow",
display_name="Top Level Workflow",
description="Determines the status of other workflows in a study",
category_id=None,
master_spec=True)
def create_spec(self, id, name, display_name="", description="", filepath=None, master_spec=False, category_id=None, display_order=None):
"""Assumes that a directory exists in static/bpmn with the same name as the given id.

View File

@ -0,0 +1 @@
,dan,lilmaker,15.04.2020 11:05,file:///home/dan/.config/libreoffice/4;

Binary file not shown.

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1v1rp1q" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_1vu5nxl" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0lvudp8</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0lvudp8" sourceRef="StartEvent_1" targetRef="Task_14svgcu" />
<bpmn:endEvent id="EndEvent_0q4qzl9">
<bpmn:incoming>SequenceFlow_02vev7n</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_02vev7n" sourceRef="Task_14svgcu" targetRef="EndEvent_0q4qzl9" />
<bpmn:userTask id="Task_14svgcu" name="Enum Lookup Form" camunda:formKey="EnumForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="AllTheNames" label="Select a value" type="enum">
<camunda:properties>
<camunda:property id="enum.options.file" value="customer_list.xls" />
<camunda:property id="enum.options.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="enum.options.label.column" value="CUSTOMER_NAME" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0lvudp8</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_02vev7n</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1vu5nxl">
<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_0lvudp8_di" bpmnElement="SequenceFlow_0lvudp8">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0q4qzl9_di" bpmnElement="EndEvent_0q4qzl9">
<dc:Bounds x="432" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_02vev7n_di" bpmnElement="SequenceFlow_02vev7n">
<di:waypoint x="370" y="117" />
<di:waypoint x="432" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_18ly1yq_di" bpmnElement="Task_14svgcu">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -14,8 +14,9 @@ class TestFilesApi(BaseTest):
def test_list_files_for_workflow_spec(self):
self.load_example_data()
spec = session.query(WorkflowSpecModel).first()
rv = self.app.get('/v1.0/file?workflow_spec_id=%s' % spec.id,
spec_id = 'core_info'
spec = session.query(WorkflowSpecModel).filter_by(id=spec_id).first()
rv = self.app.get('/v1.0/file?workflow_spec_id=%s' % spec_id,
follow_redirects=True,
content_type="application/json", headers=self.logged_in_headers())
self.assert_success(rv)

View File

@ -41,9 +41,14 @@ class TestStudyApi(BaseTest):
study = session.query(StudyModel).first()
self.assertIsNotNone(study)
def test_get_study(self):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
def test_get_study(self, mock_docs):
"""Generic test, but pretty detailed, in that the study should return a categorized list of workflows
This starts with out loading the example data, to show that all the bases are covered from ground 0."""
docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response)
new_study = self.add_test_study()
new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first()
# Add a category
@ -109,9 +114,10 @@ class TestStudyApi(BaseTest):
self.assertEqual(study.title, json_data['title'])
self.assertEqual(study.protocol_builder_status.name, json_data['protocol_builder_status'])
@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_get_all_studies(self, mock_studies, mock_details):
def test_get_all_studies(self, mock_studies, mock_details, mock_docs):
self.load_example_data()
s = StudyModel(
id=54321, # This matches one of the ids from the study_details_json data.
@ -128,6 +134,8 @@ class TestStudyApi(BaseTest):
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)
# Make the api call to get all studies
api_response = self.app.get('/v1.0/study', headers=self.logged_in_headers(), content_type="application/json")
@ -155,7 +163,12 @@ class TestStudyApi(BaseTest):
test_study = session.query(StudyModel).filter_by(id=54321).first()
self.assertFalse(test_study.inactive)
def test_get_single_study(self):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
def test_get_single_study(self, mock_docs):
docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response)
self.load_example_data()
study = session.query(StudyModel).first()
rv = self.app.get('/v1.0/study/%i' % study.id,

View File

@ -1,3 +1,6 @@
import json
from unittest.mock import patch
from crc import db
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.study import StudyModel
@ -13,10 +16,13 @@ from tests.base_test import BaseTest
class TestStudyService(BaseTest):
"""Largely tested via the test_study_api, and time is tight, but adding new tests here."""
def test_total_tasks_updated(self):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
def test_total_tasks_updated(self, mock_docs):
"""Assure that as a users progress is available when getting a list of studies for that user."""
docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response)
# Assure some basic models are in place, This is a damn mess. Our database models need an overhaul to make
# this easier - better relationship modeling is now critical.
self.load_test_spec("top_level_workflow", master_spec=True)

View File

@ -2,18 +2,15 @@ import json
import os
from crc import session, app
from crc.models.api_models import WorkflowApiSchema, Task
from crc.models.api_models import WorkflowApiSchema
from crc.models.file import FileModelSchema
from crc.models.stats import WorkflowStatsModel, TaskEventModel
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus
from crc.services.workflow_processor import WorkflowProcessor
from crc.models.workflow import WorkflowStatus
from tests.base_test import BaseTest
class TestTasksApi(BaseTest):
def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False):
rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' %
(workflow.id, str(soft_reset), str(hard_reset)),
@ -158,47 +155,6 @@ class TestTasksApi(BaseTest):
files = FileModelSchema(many=True).load(json_data, session=session)
self.assertTrue(len(files) == 1)
def test_documentation_processing_handles_replacements(self):
docs = "Some simple docs"
task = Task(1, "bill", "bill", "", "started", {}, docs, {})
task.process_documentation(docs)
self.assertEqual(docs, task.documentation)
task.data = {"replace_me": "new_thing"}
task.process_documentation("{{replace_me}}")
self.assertEqual("new_thing", task.documentation)
documentation = """
# Bigger Test
* bullet one
* bullet two has {{replace_me}}
# other stuff.
"""
expected = """
# Bigger Test
* bullet one
* bullet two has new_thing
# other stuff.
"""
task.process_documentation(documentation)
self.assertEqual(expected, task.documentation)
def test_documentation_processing_handles_conditionals(self):
docs = "This test {% if works == 'yes' %}works{% endif %}"
task = Task(1, "bill", "bill", "", "started", {}, docs, {})
task.process_documentation(docs)
self.assertEqual("This test ", task.documentation)
task.data = {"works": 'yes'}
task.process_documentation(docs)
self.assertEqual("This test works", task.documentation)
def test_get_documentation_populated_in_end(self):
self.load_example_data()
workflow = self.create_workflow('random_fact')

View File

@ -0,0 +1,68 @@
import json
import os
from crc import session, app
from crc.models.api_models import WorkflowApiSchema, Task
from crc.models.file import FileModelSchema
from crc.models.stats import WorkflowStatsModel, TaskEventModel
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
class TestWorkflowService(BaseTest):
def test_documentation_processing_handles_replacements(self):
docs = "Some simple docs"
task = Task(1, "bill", "bill", "", "started", {}, docs, {})
WorkflowService._process_documentation(task, docs)
self.assertEqual(docs, task.documentation)
task.data = {"replace_me": "new_thing"}
WorkflowService._process_documentation(task, "{{replace_me}}")
self.assertEqual("new_thing", task.documentation)
documentation = """
# Bigger Test
* bullet one
* bullet two has {{replace_me}}
# other stuff.
"""
expected = """
# Bigger Test
* bullet one
* bullet two has new_thing
# other stuff.
"""
WorkflowService._process_documentation(task,(documentation))
self.assertEqual(expected, task.documentation)
def test_documentation_processing_handles_conditionals(self):
docs = "This test {% if works == 'yes' %}works{% endif %}"
task = Task(1, "bill", "bill", "", "started", {}, docs, {})
WorkflowService._process_documentation(task, docs)
self.assertEqual("This test ", task.documentation)
task.data = {"works": 'yes'}
WorkflowService._process_documentation(task, docs)
self.assertEqual("This test works", task.documentation)
def test_enum_options_from_file(self):
self.load_example_data()
workflow = self.create_workflow('enum_options_from_file')
processor = WorkflowProcessor(workflow)
processor.do_engine_steps()
task = processor.next_task()
WorkflowService._process_options(task, task.task_spec.form.fields[0])
options = task.task_spec.form.fields[0].options
self.assertEquals(19, len(options))
self.assertEquals(1000, options[0]['id'])
self.assertEquals("UVA - INTERNAL - GM USE ONLY", options[0]['name'])