diff --git a/Pipfile.lock b/Pipfile.lock index 62b6b15e..f0e0ee3b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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": [ diff --git a/config/default.py b/config/default.py index 922dcba2..749ee944 100644 --- a/config/default.py +++ b/config/default.py @@ -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") diff --git a/crc/api/file.py b/crc/api/file.py index eb60fe82..d8d69fe4 100644 --- a/crc/api/file.py +++ b/crc/api/file.py @@ -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 diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 79885185..03a42c1d 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -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 diff --git a/crc/models/api_models.py b/crc/models/api_models.py index 30385c4b..c0889060 100644 --- a/crc/models/api_models.py +++ b/crc/models/api_models.py @@ -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: diff --git a/crc/scripts/complete_template.py b/crc/scripts/complete_template.py index 0bf51aa4..3f0161bc 100644 --- a/crc/scripts/complete_template.py +++ b/crc/scripts/complete_template.py @@ -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 diff --git a/crc/services/file_service.py b/crc/services/file_service.py index 5e331f3e..de18bb71 100644 --- a/crc/services/file_service.py +++ b/crc/services/file_service.py @@ -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 + diff --git a/crc/services/workflow_processor.py b/crc/services/workflow_processor.py index 03453c6b..e95f3c07 100644 --- a/crc/services/workflow_processor.py +++ b/crc/services/workflow_processor.py @@ -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): diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py new file mode 100644 index 00000000..0937cc75 --- /dev/null +++ b/crc/services/workflow_service.py @@ -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]}) diff --git a/crc/static/bpmn/finance/finance.bpmn b/crc/static/bpmn/finance/finance.bpmn index 51a513c1..e24fe802 100644 --- a/crc/static/bpmn/finance/finance.bpmn +++ b/crc/static/bpmn/finance/finance.bpmn @@ -14,13 +14,13 @@ - + - + @@ -54,7 +54,7 @@ - #### Process: + #### 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 #### Non-Funded Executed Agreement -#### Process: +#### Process: OGC will upload the Non-Funded Executed Agreement after it has been negotiated by OSP contract negotiator. diff --git a/crc/static/bpmn/ids_full_submission/investigators_brochure.dmn b/crc/static/bpmn/ids_full_submission/investigators_brochure.dmn index 32b80410..c581f066 100644 --- a/crc/static/bpmn/ids_full_submission/investigators_brochure.dmn +++ b/crc/static/bpmn/ids_full_submission/investigators_brochure.dmn @@ -7,7 +7,7 @@ - Documents["DrugDevDoc_InvestBrochure"]["count"] + Documents.DrugDevDoc_InvestBrochure.count diff --git a/crc/static/bpmn/ids_full_submission/ivrs_iwrs_ixrs.dmn b/crc/static/bpmn/ids_full_submission/ivrs_iwrs_ixrs.dmn index 82daa151..39f0d20a 100644 --- a/crc/static/bpmn/ids_full_submission/ivrs_iwrs_ixrs.dmn +++ b/crc/static/bpmn/ids_full_submission/ivrs_iwrs_ixrs.dmn @@ -7,7 +7,7 @@ - Documents["DrugDevDoc_IVRSIWRSIXRSMan"]["count"] + Documents.DrugDevDoc_IVRSIWRSIXRSMan.count diff --git a/crc/static/bpmn/irb_api_personnel/coordinator_status.dmn b/crc/static/bpmn/irb_api_personnel/coordinator_status.dmn new file mode 100644 index 00000000..855bc719 --- /dev/null +++ b/crc/static/bpmn/irb_api_personnel/coordinator_status.dmn @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + "Placeholder" + + + + + diff --git a/crc/static/bpmn/irb_api_personnel/department_chair_status.dmn b/crc/static/bpmn/irb_api_personnel/department_chair_status.dmn new file mode 100644 index 00000000..7579049e --- /dev/null +++ b/crc/static/bpmn/irb_api_personnel/department_chair_status.dmn @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + "DC Placeholder" + + + + + diff --git a/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn b/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn index 6417ed2c..33aa0434 100644 --- a/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn +++ b/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn @@ -10,14 +10,7 @@ StudyInfo investigators - ### 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 %} + {{ElementDoc_PrimaryInvestigator}} @@ -44,7 +37,7 @@ The PI is needed for many required steps. Please enter this information in Prot - Flow_05aywbq + Flow_19i1d30 Flow_0g0o593 Flow_1nudg96 Flow_18ix81l @@ -58,13 +51,6 @@ The PI is needed for many required steps. Please enter this information in Prot - ### 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 %} @@ -77,13 +63,6 @@ The PI is needed for many required steps. Please enter this information in Prot - ### 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 %} @@ -94,74 +73,110 @@ The PI is needed for many required steps. Please enter this information in Prot Flow_0y1jvdw - + + + + Flow_05aywbq + Flow_12rh5aj + + + + Flow_12rh5aj + Flow_04nzqn8 + + + + Flow_04nzqn8 + Flow_19i1d30 + + + + + + + + + + + + + - - + + - - + + - - - + + + - - - + + + - - + + - - + + - - + + - - - + + + - - - + + + - + - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crc/static/bpmn/irb_api_personnel/primary_investigator_status.dmn b/crc/static/bpmn/irb_api_personnel/primary_investigator_status.dmn new file mode 100644 index 00000000..88baa49d --- /dev/null +++ b/crc/static/bpmn/irb_api_personnel/primary_investigator_status.dmn @@ -0,0 +1,32 @@ + + + + + + + + + + list contains( for i in [study.investigators[0].INVESTIGATORTYPE, study.investigators[1].INVESTIGATORTYPE, study.investigators[2].INVESTIGATORTYPE] return i, "PI") + + + + + + true + + + "Placeholder - True" + + + + + false + + + "Placeholder - False" + + + + + diff --git a/crc/static/bpmn/top_level_workflow/data_security_plan.dmn b/crc/static/bpmn/top_level_workflow/data_security_plan.dmn index d67da3b2..dd84045f 100644 --- a/crc/static/bpmn/top_level_workflow/data_security_plan.dmn +++ b/crc/static/bpmn/top_level_workflow/data_security_plan.dmn @@ -2,12 +2,12 @@ - + - + - Documents['Study_DataSecurityPlan']['required'] + Documents['UVACompl_PRCAppr']['required'] diff --git a/crc/static/bpmn/top_level_workflow/enter_core_info.dmn b/crc/static/bpmn/top_level_workflow/enter_core_info.dmn index d4345af3..7a204622 100644 --- a/crc/static/bpmn/top_level_workflow/enter_core_info.dmn +++ b/crc/static/bpmn/top_level_workflow/enter_core_info.dmn @@ -2,7 +2,7 @@ - + diff --git a/crc/static/bpmn/top_level_workflow/sponsor_funding_source.dmn b/crc/static/bpmn/top_level_workflow/sponsor_funding_source.dmn index e66274d3..2cf19dca 100644 --- a/crc/static/bpmn/top_level_workflow/sponsor_funding_source.dmn +++ b/crc/static/bpmn/top_level_workflow/sponsor_funding_source.dmn @@ -2,10 +2,10 @@