Merge branch 'feature/protocol_status' into feature/previous_task

# Conflicts:
#	crc/services/study_service.py
This commit is contained in:
Dan Funk 2020-05-04 11:08:36 -04:00
commit 714b5f3be0
28 changed files with 356 additions and 340 deletions

View File

@ -5,7 +5,7 @@ from jinja2 import UndefinedError
from crc import session from crc import session
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES
from crc.models.workflow import WorkflowSpecModel from crc.models.workflow import WorkflowSpecModel, WorkflowModel
from docxtpl import DocxTemplate from docxtpl import DocxTemplate
import jinja2 import jinja2
@ -33,11 +33,12 @@ Takes two arguments:
def do_task(self, task, study_id, *args, **kwargs): def do_task(self, task, study_id, *args, **kwargs):
workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
final_document_stream = self.process_template(task, study_id, *args, **kwargs) final_document_stream = self.process_template(task, study_id, *args, **kwargs)
workflow = session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
file_name = args[0] file_name = args[0]
irb_doc_code = args[1] irb_doc_code = args[1]
FileService.add_task_file(study_id=study_id, FileService.add_task_file(study_id=study_id,
workflow_id=workflow_id, workflow_id=workflow_id,
workflow_spec_id=workflow.workflow_spec_id,
task_id=task.id, task_id=task.id,
name=file_name, name=file_name,
content_type=CONTENT_TYPES['docx'], content_type=CONTENT_TYPES['docx'],

View File

@ -1,81 +0,0 @@
from crc.api.common import ApiError
from crc.scripts.script import Script, ScriptValidationError
from crc.services.file_service import FileService
from crc.services.protocol_builder import ProtocolBuilderService
class Documents(Script):
"""Provides information about the documents required by Protocol Builder."""
pb = ProtocolBuilderService()
def get_description(self):
return """
Provides detailed information about the documents loaded as a part of completing tasks.
Makes an immediate call to the IRB Protocol Builder API to get a list of currently required
documents. It then collects all the information in a reference file called 'irb_pro_categories.xls',
if the Id from Protocol Builder matches an Id in this table, all data available in that row
is also provided.
This place a dictionary of values in the current task, where the key is the code in the lookup table.
For example:
``` "Documents" :
{
"UVACompliance_PRCApproval": {
"name": "Cancer Center's PRC Approval Form",
"category1": "UVA Compliance",
"category2": "PRC Approval",
"category3": "",
"Who Uploads?": "CRC",
"required": True,
"Id": 6
"count": 0
},
24: { ...
}
```
"""
def do_task_validate_only(self, task, study_id, *args, **kwargs):
"""For validation only, pretend no results come back from pb"""
pb_docs = []
self.add_data_to_task(task, self.get_documents(study_id, pb_docs))
def do_task(self, task, study_id, *args, **kwargs):
"""Takes data from the protocol builder, and merges it with data from the IRB Pro Categories
spreadsheet to return pertinent details about the required documents."""
pb_docs = self.pb.get_required_docs(study_id, as_objects=True)
self.add_data_to_task(task, self.get_documents(study_id, pb_docs))
def get_documents(self, study_id, pb_docs):
"""Takes data from the protocol builder, and merges it with data from the IRB Pro Categories spreadsheet to return
pertinent details about the required documents."""
doc_dictionary = FileService.get_file_reference_dictionary()
required_docs = {}
for code, required_doc in doc_dictionary.items():
try:
pb_data = next((item for item in pb_docs if int(item.AUXDOCID) == int(required_doc['Id'])), None)
except:
pb_data = None
required_doc['count'] = self.get_count(study_id, code)
required_doc['required'] = False
if pb_data:
required_doc['required'] = True
required_docs[code] = required_doc
return required_docs
def get_count(self, study_id, irb_doc_code):
"""Returns the total number of documents that have been uploaded that match
the given document id. """
return(len(FileService.get_files(study_id=study_id, irb_doc_code=irb_doc_code)))
# Verifies that information is available for this script task to function
# correctly. Returns a list of validation errors.
@staticmethod
def validate():
errors = []
try:
dict = FileService.get_file_reference_dictionary()
except ApiError as ae:
errors.append(ScriptValidationError.from_api_error(ae))
return errors

View File

@ -24,13 +24,6 @@ class Script(object):
"does must provide a validate_only option that mimics the do_task, " + "does must provide a validate_only option that mimics the do_task, " +
"but does not make external calls or database updates." ) "but does not make external calls or database updates." )
def validate(self):
"""Override this method to perform an early check that the script has access to
everything it needs to properly process requests.
Should return an array of ScriptValidationErrors.
"""
return []
@staticmethod @staticmethod
def get_all_subclasses(): def get_all_subclasses():
return Script._get_all_subclasses(Script) return Script._get_all_subclasses(Script)

View File

@ -4,7 +4,8 @@ from crc import session, app
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.study import StudyModel, StudySchema from crc.models.study import StudyModel, StudySchema
from crc.models.workflow import WorkflowStatus from crc.models.workflow import WorkflowStatus
from crc.scripts.script import Script from crc.scripts.script import Script, ScriptValidationError
from crc.services.file_service import FileService
from crc.services.ldap_service import LdapService from crc.services.ldap_service import LdapService
from crc.services.protocol_builder import ProtocolBuilderService from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.study_service import StudyService from crc.services.study_service import StudyService
@ -15,21 +16,24 @@ class StudyInfo(Script):
"""Just your basic class that can pull in data from a few api endpoints and do a basic task.""" """Just your basic class that can pull in data from a few api endpoints and do a basic task."""
pb = ProtocolBuilderService() pb = ProtocolBuilderService()
type_options = ['info', 'investigators', 'details', 'approvals', 'documents_status'] type_options = ['info', 'investigators', 'details', 'approvals', 'documents', 'protocol']
def get_description(self): def get_description(self):
return """StudyInfo [TYPE], where TYPE is one of 'info', 'investigators', or 'details' 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 Adds details about the current study to the Task Data. The type of information required should be
provided as an argument. Basic returns the basic information such as the title. Investigators provides 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 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 'required_docs', of details about the study, as gathered within the protocol builder, and 'documents',
lists all the documents the Protocol Builder has determined will be required as a part of lists all the documents that can be a part of the study, with documents from Protocol Builder
this study. marked as required, and details about any files that were uploaded' .
""" """
def do_task_validate_only(self, task, study_id, *args, **kwargs): def do_task_validate_only(self, task, study_id, *args, **kwargs):
"""For validation only, pretend no results come back from pb""" """For validation only, pretend no results come back from pb"""
self.check_args(args) 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()
data = { data = {
"study":{ "study":{
"info": { "info": {
@ -57,7 +61,8 @@ class StudyInfo(Script):
"status": WorkflowStatus.not_started.value, "status": WorkflowStatus.not_started.value,
"workflow_spec_id": "irb_api_details", "workflow_spec_id": "irb_api_details",
}, },
"documents_status": [ "documents": {
"AD_CoCApp":
{ {
'category1': 'Ancillary Document', 'category1': 'Ancillary Document',
'category2': 'CoC Application', 'category2': 'CoC Application',
@ -75,7 +80,10 @@ class StudyInfo(Script):
'workflow_spec_id': 'irb_api_details', 'workflow_spec_id': 'irb_api_details',
'status': 'complete', 'status': 'complete',
} }
] },
'protocol': {
id: 0,
}
} }
} }
self.add_data_to_task(task=task, data=data["study"]) self.add_data_to_task(task=task, data=data["study"])
@ -99,8 +107,10 @@ class StudyInfo(Script):
self.add_data_to_task(task, {cmd: self.pb.get_study_details(study_id)}) self.add_data_to_task(task, {cmd: self.pb.get_study_details(study_id)})
if cmd == 'approvals': if cmd == 'approvals':
self.add_data_to_task(task, {cmd: StudyService().get_approvals(study_id)}) self.add_data_to_task(task, {cmd: StudyService().get_approvals(study_id)})
if cmd == 'documents_status': if cmd == 'documents':
self.add_data_to_task(task, {cmd: StudyService().get_documents_status(study_id)}) self.add_data_to_task(task, {cmd: StudyService().get_documents_status(study_id)})
if cmd == 'protocol':
self.add_data_to_task(task, {cmd: StudyService().get_protocol(study_id)})
def check_args(self, args): def check_args(self, args):

View File

@ -56,7 +56,7 @@ class FileService(object):
data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE) data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE)
xls = ExcelFile(data_model.data) xls = ExcelFile(data_model.data)
df = xls.parse(xls.sheet_names[0]) df = xls.parse(xls.sheet_names[0])
return code in df['Code'].values return code in df['code'].values
@staticmethod @staticmethod
def get_file_reference_dictionary(): def get_file_reference_dictionary():
@ -65,11 +65,11 @@ class FileService(object):
data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE) data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE)
xls = ExcelFile(data_model.data) xls = ExcelFile(data_model.data)
df = xls.parse(xls.sheet_names[0]) df = xls.parse(xls.sheet_names[0])
df['Id'] = df['Id'].fillna(0) df['id'] = df['id'].fillna(0)
df = df.astype({'Id': 'Int64'}) df = df.astype({'id': 'Int64'})
df = df.fillna('') df = df.fillna('')
df = df.applymap(str) df = df.applymap(str)
df = df.set_index('Code') df = df.set_index('code')
# IF we need to convert the column names to something more sensible. # IF we need to convert the column names to something more sensible.
# df.columns = [snakeCase(x) for x in df.columns] # df.columns = [snakeCase(x) for x in df.columns]
return json.loads(df.to_json(orient='index')) return json.loads(df.to_json(orient='index'))
@ -80,12 +80,13 @@ class FileService(object):
# all_dict = df.set_index('Id').to_dict('index') # all_dict = df.set_index('Id').to_dict('index')
@staticmethod @staticmethod
def add_task_file(study_id, workflow_id, task_id, name, content_type, binary_data, def add_task_file(study_id, workflow_id, workflow_spec_id, task_id, name, content_type, binary_data,
irb_doc_code=None): irb_doc_code=None):
"""Create a new file and associate it with an executing task within a workflow.""" """Create a new file and associate it with an executing task within a workflow."""
file_model = FileModel( file_model = FileModel(
study_id=study_id, study_id=study_id,
workflow_id=workflow_id, workflow_id=workflow_id,
workflow_spec_id=workflow_spec_id,
task_id=task_id, task_id=task_id,
name=name, name=name,
irb_doc_code=irb_doc_code irb_doc_code=irb_doc_code
@ -168,7 +169,7 @@ class FileService(object):
if form_field_key: if form_field_key:
query = query.filter_by(form_field_key=form_field_key) query = query.filter_by(form_field_key=form_field_key)
if name: if name:
query = query.filter_by(name=form_field_key) query = query.filter_by(name=name)
if irb_doc_code: if irb_doc_code:
query = query.filter_by(irb_doc_code=irb_doc_code) query = query.filter_by(irb_doc_code=irb_doc_code)

View File

@ -41,13 +41,10 @@ class ProtocolBuilderService(object):
(response.status_code, response.text)) (response.status_code, response.text))
@staticmethod @staticmethod
def get_required_docs(study_id, as_objects=False) -> Optional[List[ProtocolBuilderRequiredDocument]]: def get_required_docs(study_id) -> Optional[List[ProtocolBuilderRequiredDocument]]:
ProtocolBuilderService.check_args(study_id) ProtocolBuilderService.check_args(study_id)
response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id) response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id)
if response.ok and response.text: if response.ok and response.text:
if as_objects:
return ProtocolBuilderRequiredDocumentSchema(many=True).loads(response.text)
else:
return json.loads(response.text) return json.loads(response.text)
else: else:
raise ApiError("protocol_builder_error", raise ApiError("protocol_builder_error",

View File

@ -1,17 +1,17 @@
from datetime import datetime from datetime import datetime
import json
from typing import List from typing import List
from SpiffWorkflow import WorkflowException from SpiffWorkflow import WorkflowException
from crc import db, session from crc import db, session
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.file import FileModel from crc.models.file import FileModel, FileModelSchema
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
from crc.models.stats import TaskEventModel from crc.models.stats import TaskEventModel
from crc.models.study import StudyModel, Study, Category, WorkflowMetadata from crc.models.study import StudyModel, Study, Category, WorkflowMetadata
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \ from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus WorkflowStatus
from crc.scripts.documents import Documents
from crc.services.file_service import FileService from crc.services.file_service import FileService
from crc.services.protocol_builder import ProtocolBuilderService from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
@ -39,6 +39,9 @@ class StudyService(object):
study = Study.from_model(study_model) study = Study.from_model(study_model)
study.categories = StudyService.get_categories() study.categories = StudyService.get_categories()
workflow_metas = StudyService.__get_workflow_metas(study_id) workflow_metas = StudyService.__get_workflow_metas(study_id)
# Calling this line repeatedly is very very slow. It creates the
# master spec and runs it.
status = StudyService.__get_study_status(study_model) status = StudyService.__get_study_status(study_model)
study.warnings = StudyService.__update_status_of_workflow_meta(workflow_metas, status) study.warnings = StudyService.__update_status_of_workflow_meta(workflow_metas, status)
@ -101,52 +104,63 @@ class StudyService(object):
@staticmethod @staticmethod
def get_documents_status(study_id): def get_documents_status(study_id):
"""Returns a list of required documents and related workflow status.""" """Returns a list of documents related to the study, if they are required, and any file information
doc_service = Documents() that is available.."""
# Get PB required docs # Get PB required docs
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id, as_objects=True) pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
# Get required docs for study # Loop through all known document types, get the counts for those files, and use pb_docs to mark those required.
study_docs = doc_service.get_documents(study_id=study_id, pb_docs=pb_docs) doc_dictionary = FileService.get_file_reference_dictionary()
documents = {}
# Container for results for code, doc in doc_dictionary.items():
documents = []
# For each required doc, get file(s)
for code, doc in study_docs.items():
if not doc['required']:
continue
pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None)
doc['required'] = False
if pb_data:
doc['required'] = True
doc['study_id'] = study_id doc['study_id'] = study_id
doc['code'] = code doc['code'] = code
# Make a display name out of categories if none exists # Make a display name out of categories
if 'Name' in doc and len(doc['Name']) > 0:
doc['display_name'] = doc['Name']
else:
name_list = [] name_list = []
for cat_key in ['category1', 'category2', 'category3']: for cat_key in ['category1', 'category2', 'category3']:
if doc[cat_key] not in ['', 'NULL']: if doc[cat_key] not in ['', 'NULL']:
name_list.append(doc[cat_key]) name_list.append(doc[cat_key])
doc['display_name'] = ' '.join(name_list) doc['display_name'] = ' / '.join(name_list)
# For each file, get associated workflow status # For each file, get associated workflow status
doc_files = FileService.get_files(study_id=study_id, irb_doc_code=code) doc_files = FileService.get_files(study_id=study_id, irb_doc_code=code)
doc['count'] = len(doc_files)
doc['files'] = []
for file in doc_files: for file in doc_files:
doc['file_id'] = file.id doc['files'].append({'file_id': file.id,
doc['task_id'] = file.task_id 'task_id': file.task_id,
doc['workflow_id'] = file.workflow_id 'workflow_id': file.workflow_id,
doc['workflow_spec_id'] = file.workflow_spec_id 'workflow_spec_id': file.workflow_spec_id})
if doc['status'] is None: # update the document status to match the status of the workflow it is in.
if not 'status' in doc or doc['status'] is None:
workflow: WorkflowModel = session.query(WorkflowModel).filter_by(id=file.workflow_id).first() workflow: WorkflowModel = session.query(WorkflowModel).filter_by(id=file.workflow_id).first()
doc['status'] = workflow.status.value doc['status'] = workflow.status.value
documents.append(doc) documents[code] = doc
return documents return documents
@staticmethod
def get_protocol(study_id):
"""Returns the study protocol, if it has been uploaded."""
file = db.session.query(FileModel)\
.filter_by(study_id=study_id)\
.filter_by(form_field_key='Study_Protocol_Document')\
.first()
return FileModelSchema().dump(file)
@staticmethod @staticmethod
def synch_all_studies_with_protocol_builder(user): def synch_all_studies_with_protocol_builder(user):
"""Assures that the studies we have locally for the given user are """Assures that the studies we have locally for the given user are

View File

@ -13,23 +13,29 @@
<bpmn:documentation># Documents &amp; Approvals <bpmn:documentation># Documents &amp; Approvals
&gt; ## Protocol Document Management &gt; ## Protocol Document Management
&gt; [Upload Protocol Here](/) {% if StudyInfo.protocol is defined -%}
{%- set p = StudyInfo.protocol -%}
&gt; [{{p.name}}](/study/{{p.study_id}}/workflow/{{p.workflow_id}}/task/{{p.task_id}})
{%- else -%}
&gt; No protocol uploaded yet.
{% endif %}
&gt; ## Approvals &gt; ## Approvals
&gt; | Name | Status | Help | &gt; | Name | Status | Help |
|:---- |:------ |:---- | |:---- |:------ |:---- |
{% for approval in StudyInfo.approvals -%} {% for approval in StudyInfo.approvals -%}
| [{{approval.display_name}}](/study/{{approval.study_id}}/workflow/{{approval.workflow_id}}) | {{approval.status}} | [Context here](/help/{{approval.workflow_spec_id}}) | | [{{approval.display_name}}](/study/{{approval.study_id}}/workflow/{{approval.workflow_id}}) | {{approval.status}} | [?](/help/{{approval.workflow_spec_id}}) |
{% endfor %} {% endfor %}
&gt; ## Documents &gt; ## Documents
&gt; | Name | Status | Help | Download | &gt; | Name | Status | Help | Download |
|:---- |:------ |:---- |:-------- | |:---- |:------ |:---- |:-------- |
{% for doc in StudyInfo.documents_status -%} {% for doc in StudyInfo.documents -%}
{% if doc.file_id is defined -%} {% if doc.file_id is defined -%}
| [{{doc.display_name}}](/study/{{doc.study_id}}/workflow/{{doc.workflow_id}}/task/{{doc.task_id}}) | {{doc.status}} | [Context here](/help/documents/{{doc.code}}) | [Download](/file/{{doc.file_id}}) | | [{{doc.display_name}}](/study/{{doc.study_id}}/workflow/{{doc.workflow_id}}/task/{{doc.task_id}}) | {{doc.status}} | [Context here](/help/documents/{{doc.code}}) | [Download](/file/{{doc.file_id}}/data) |
{%- else -%} {%- else -%}
| {{doc.display_name}} | Not started | [Context here](/help/documents/{{doc.code}}) | No file yet | | {{doc.display_name}} | Not started | [?](/help/documents/{{doc.code}}) | No file yet |
{%- endif %} {%- endif %}
{% endfor %}</bpmn:documentation> {% endfor %}</bpmn:documentation>
<bpmn:extensionElements> <bpmn:extensionElements>
@ -46,47 +52,60 @@
<bpmn:script>StudyInfo approvals</bpmn:script> <bpmn:script>StudyInfo approvals</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:scriptTask id="Activity_1aju60t" name="Load Documents"> <bpmn:scriptTask id="Activity_1aju60t" name="Load Documents">
<bpmn:incoming>Flow_1k3su2q</bpmn:incoming> <bpmn:incoming>Flow_0w20w9j</bpmn:incoming>
<bpmn:outgoing>Flow_0c7ryff</bpmn:outgoing> <bpmn:outgoing>Flow_0c7ryff</bpmn:outgoing>
<bpmn:script>StudyInfo documents_status</bpmn:script> <bpmn:script>StudyInfo documents_status</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_142jtxs" sourceRef="Activity_0a14x7j" targetRef="Activity_DisplayDocsAndApprovals" /> <bpmn:sequenceFlow id="Flow_142jtxs" sourceRef="Activity_0a14x7j" targetRef="Activity_DisplayDocsAndApprovals" />
<bpmn:sequenceFlow id="Flow_0c7ryff" sourceRef="Activity_1aju60t" targetRef="Activity_0a14x7j" /> <bpmn:sequenceFlow id="Flow_0c7ryff" sourceRef="Activity_1aju60t" targetRef="Activity_0a14x7j" />
<bpmn:sequenceFlow id="Flow_1k3su2q" sourceRef="StartEvent_1" targetRef="Activity_1aju60t" /> <bpmn:sequenceFlow id="Flow_1k3su2q" sourceRef="StartEvent_1" targetRef="Activity_0b4ojeq" />
<bpmn:scriptTask id="Activity_0b4ojeq" name="Load Protocol">
<bpmn:incoming>Flow_1k3su2q</bpmn:incoming>
<bpmn:outgoing>Flow_0w20w9j</bpmn:outgoing>
<bpmn:script>StudyInfo protocol</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0w20w9j" sourceRef="Activity_0b4ojeq" targetRef="Activity_1aju60t" />
</bpmn:process> </bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1gmf4la"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1gmf4la">
<bpmndi:BPMNEdge id="Flow_1k3su2q_di" bpmnElement="Flow_1k3su2q"> <bpmndi:BPMNEdge id="Flow_1k3su2q_di" bpmnElement="Flow_1k3su2q">
<di:waypoint x="228" y="117" /> <di:waypoint x="188" y="117" />
<di:waypoint x="290" y="117" /> <di:waypoint x="240" y="117" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c7ryff_di" bpmnElement="Flow_0c7ryff"> <bpmndi:BPMNEdge id="Flow_0c7ryff_di" bpmnElement="Flow_0c7ryff">
<di:waypoint x="390" y="117" /> <di:waypoint x="490" y="117" />
<di:waypoint x="440" y="117" /> <di:waypoint x="540" y="117" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_142jtxs_di" bpmnElement="Flow_142jtxs"> <bpmndi:BPMNEdge id="Flow_142jtxs_di" bpmnElement="Flow_142jtxs">
<di:waypoint x="540" y="117" /> <di:waypoint x="640" y="117" />
<di:waypoint x="610" y="117" /> <di:waypoint x="710" y="117" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0m7unlb_di" bpmnElement="Flow_0m7unlb"> <bpmndi:BPMNEdge id="Flow_0m7unlb_di" bpmnElement="Flow_0m7unlb">
<di:waypoint x="710" y="117" /> <di:waypoint x="810" y="117" />
<di:waypoint x="782" y="117" /> <di:waypoint x="882" y="117" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="192" y="99" width="36" height="36" /> <dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1qvyxg7_di" bpmnElement="EndEvent_1qvyxg7"> <bpmndi:BPMNShape id="EndEvent_1qvyxg7_di" bpmnElement="EndEvent_1qvyxg7">
<dc:Bounds x="782" y="99" width="36" height="36" /> <dc:Bounds x="882" y="99" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_19nawos_di" bpmnElement="Activity_DisplayDocsAndApprovals"> <bpmndi:BPMNShape id="Activity_19nawos_di" bpmnElement="Activity_DisplayDocsAndApprovals">
<dc:Bounds x="610" y="77" width="100" height="80" /> <dc:Bounds x="710" y="77" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1bxk8h3_di" bpmnElement="Activity_0a14x7j"> <bpmndi:BPMNShape id="Activity_1bxk8h3_di" bpmnElement="Activity_0a14x7j">
<dc:Bounds x="440" y="77" width="100" height="80" /> <dc:Bounds x="540" y="77" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_07ytvmv_di" bpmnElement="Activity_1aju60t"> <bpmndi:BPMNShape id="Activity_07ytvmv_di" bpmnElement="Activity_1aju60t">
<dc:Bounds x="290" y="77" width="100" height="80" /> <dc:Bounds x="390" y="77" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0wf6u3m_di" bpmnElement="Activity_0b4ojeq">
<dc:Bounds x="240" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0w20w9j_di" bpmnElement="Flow_0w20w9j">
<di:waypoint x="340" y="117" />
<di:waypoint x="390" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>
</bpmn:definitions> </bpmn:definitions>

View File

@ -217,7 +217,7 @@ Protocol Owner: **(need to insert value here)**</bpmn:documentation>
<bpmn:scriptTask id="Activity_LoadDocuments" name="Load Documents"> <bpmn:scriptTask id="Activity_LoadDocuments" name="Load Documents">
<bpmn:incoming>SequenceFlow_1dexemq</bpmn:incoming> <bpmn:incoming>SequenceFlow_1dexemq</bpmn:incoming>
<bpmn:outgoing>Flow_1x9d2mo</bpmn:outgoing> <bpmn:outgoing>Flow_1x9d2mo</bpmn:outgoing>
<bpmn:script>Documents</bpmn:script> <bpmn:script>StudyInfo documents</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
</bpmn:process> </bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="input_1" label="Investigator&#39;s Brochure Form Upload Count"> <input id="input_1" label="Investigator&#39;s Brochure Form Upload Count">
<inputExpression id="inputExpression_1" typeRef="integer"> <inputExpression id="inputExpression_1" typeRef="integer">
<text>Documents.DrugDevDoc_InvestBrochure.count</text> <text>StudyInfo.documents.DrugDevDoc_InvestBrochure.count</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="Investigator&#39;s Brochure(s) Uploaded?" name="isInvestigatorsBrochure" typeRef="boolean" /> <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"> <decisionTable id="decisionTable_1">
<input id="input_1" label="IVRS-IWRS-IXRS Manual Count"> <input id="input_1" label="IVRS-IWRS-IXRS Manual Count">
<inputExpression id="inputExpression_1" typeRef="integer"> <inputExpression id="inputExpression_1" typeRef="integer">
<text>Documents.DrugDevDoc_IVRSIWRSIXRSMan.count</text> <text>StudyInfo.documents.DrugDevDoc_IVRSIWRSIXRSMan.count</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="IVRS-IWRS-IXRS Manual Uploaded?" name="isIVRS-IWRS-IXRS" typeRef="boolean" /> <output id="output_1" label="IVRS-IWRS-IXRS Manual Uploaded?" name="isIVRS-IWRS-IXRS" typeRef="boolean" />

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="input_1" label="Pharmacy Manual Upload Count"> <input id="input_1" label="Pharmacy Manual Upload Count">
<inputExpression id="inputExpression_1" typeRef="integer"> <inputExpression id="inputExpression_1" typeRef="integer">
<text>Documents["DrugDevDoc_PharmManual"]["count"]</text> <text>StudyInfo.documents.DrugDevDoc_PharmManual.count</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="Pharmacy Manual(s) Uploaded?" name="isPharmacyManual" typeRef="boolean" /> <output id="output_1" label="Pharmacy Manual(s) Uploaded?" name="isPharmacyManual" typeRef="boolean" />

View File

@ -7,7 +7,7 @@
<decisionTable id="DecisionTable_1mjqwlv"> <decisionTable id="DecisionTable_1mjqwlv">
<input id="InputClause_18pwfqu" label="Required Doc Keys"> <input id="InputClause_18pwfqu" label="Required Doc Keys">
<inputExpression id="LiteralExpression_1y84stb" typeRef="boolean" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1y84stb" typeRef="boolean" expressionLanguage="feel">
<text>Documents['Study_DataSecurityPlan']['required']</text> <text>StudyInfo.documents.Study_DataSecurityPlan.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" /> <output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" />

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="InputClause_1ki80j6" label="required doc ids"> <input id="InputClause_1ki80j6" label="required doc ids">
<inputExpression id="LiteralExpression_10mfcy7" typeRef="boolean" expressionLanguage="Python"> <inputExpression id="LiteralExpression_10mfcy7" typeRef="boolean" expressionLanguage="Python">
<text>Documents['UVACompl_PRCAppr']['required']</text> <text>StudyInfo.documents.UVACompl_PRCAppr.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" /> <output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" />

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="input_1" label="IRB API IDS Waiver Status"> <input id="input_1" label="IRB API IDS Waiver Status">
<inputExpression id="inputExpression_1" typeRef="boolean"> <inputExpression id="inputExpression_1" typeRef="boolean">
<text>Documents.UVACompl_IDSWaiverApp.required</text> <text>StudyInfo.documents.UVACompl_IDSWaiverApp.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="Menu Status" name="ids_full_submission" typeRef="string" /> <output id="output_1" label="Menu Status" name="ids_full_submission" typeRef="string" />

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="input_1" label="IRB API Input"> <input id="input_1" label="IRB API Input">
<inputExpression id="inputExpression_1" typeRef="boolean"> <inputExpression id="inputExpression_1" typeRef="boolean">
<text>Documents.UVACompl_IDSWaiverApp.required</text> <text>StudyInfo.documents.UVACompl_IDSWaiverApp.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="Menu State" name="ids_waiver" typeRef="string" /> <output id="output_1" label="Menu State" name="ids_waiver" typeRef="string" />

View File

@ -7,7 +7,7 @@
<decisionTable id="DecisionTable_00zdxg0"> <decisionTable id="DecisionTable_00zdxg0">
<input id="InputClause_02n3ccs" label="CoCApplication Required?"> <input id="InputClause_02n3ccs" label="CoCApplication Required?">
<inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel">
<text>Documents['AD_LabManual']['required']</text> <text>StudyInfo.documents.AD_LabManual.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_1ybi1ud" label="sponsor_funding_source" name="sponsor_funding_source" typeRef="string" /> <output id="OutputClause_1ybi1ud" label="sponsor_funding_source" name="sponsor_funding_source" typeRef="string" />

View File

@ -11,7 +11,7 @@
<bpmn:scriptTask id="Task_Load_Requirements" name="Load Documents From PB"> <bpmn:scriptTask id="Task_Load_Requirements" name="Load Documents From PB">
<bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming> <bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing> <bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>Documents</bpmn:script> <bpmn:script>StudyInfo documents</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info"> <bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<bpmn:incoming>Flow_1m8285h</bpmn:incoming> <bpmn:incoming>Flow_1m8285h</bpmn:incoming>
@ -109,159 +109,159 @@
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0jhpidf"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0jhpidf">
<bpmndi:BPMNEdge id="Flow_1ybicki_di" bpmnElement="Flow_1ybicki"> <bpmndi:BPMNEdge id="Flow_1ybicki_di" bpmnElement="Flow_1ybicki">
<di:waypoint x="1630" y="439" /> <di:waypoint x="1540" y="439" />
<di:waypoint x="1712" y="439" /> <di:waypoint x="1622" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vo6ul1_di" bpmnElement="Flow_0vo6ul1"> <bpmndi:BPMNEdge id="Flow_0vo6ul1_di" bpmnElement="Flow_0vo6ul1">
<di:waypoint x="1460" y="439" /> <di:waypoint x="1370" y="439" />
<di:waypoint x="1530" y="439" /> <di:waypoint x="1440" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1qyrmzn_di" bpmnElement="Flow_1qyrmzn"> <bpmndi:BPMNEdge id="Flow_1qyrmzn_di" bpmnElement="Flow_1qyrmzn">
<di:waypoint x="1305" y="439" /> <di:waypoint x="1215" y="439" />
<di:waypoint x="1360" y="439" /> <di:waypoint x="1270" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ffvg2f_di" bpmnElement="Flow_0ffvg2f"> <bpmndi:BPMNEdge id="Flow_0ffvg2f_di" bpmnElement="Flow_0ffvg2f">
<di:waypoint x="1220" y="530" /> <di:waypoint x="1130" y="530" />
<di:waypoint x="1280" y="530" /> <di:waypoint x="1190" y="530" />
<di:waypoint x="1280" y="464" /> <di:waypoint x="1190" y="464" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_145qxh8_di" bpmnElement="Flow_145qxh8"> <bpmndi:BPMNEdge id="Flow_145qxh8_di" bpmnElement="Flow_145qxh8">
<di:waypoint x="1210" y="360" /> <di:waypoint x="1120" y="360" />
<di:waypoint x="1280" y="360" /> <di:waypoint x="1190" y="360" />
<di:waypoint x="1280" y="414" /> <di:waypoint x="1190" y="414" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1jini69_di" bpmnElement="Flow_1jini69"> <bpmndi:BPMNEdge id="Flow_1jini69_di" bpmnElement="Flow_1jini69">
<di:waypoint x="1040" y="414" /> <di:waypoint x="950" y="414" />
<di:waypoint x="1040" y="360" /> <di:waypoint x="950" y="360" />
<di:waypoint x="1110" y="360" /> <di:waypoint x="1020" y="360" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_14ce1d7_di" bpmnElement="Flow_14ce1d7"> <bpmndi:BPMNEdge id="Flow_14ce1d7_di" bpmnElement="Flow_14ce1d7">
<di:waypoint x="1040" y="464" /> <di:waypoint x="950" y="464" />
<di:waypoint x="1040" y="530" /> <di:waypoint x="950" y="530" />
<di:waypoint x="1120" y="530" /> <di:waypoint x="1030" y="530" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0eq6px2_di" bpmnElement="Flow_0eq6px2"> <bpmndi:BPMNEdge id="Flow_0eq6px2_di" bpmnElement="Flow_0eq6px2">
<di:waypoint x="980" y="439" /> <di:waypoint x="890" y="439" />
<di:waypoint x="1015" y="439" /> <di:waypoint x="925" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18pax8n_di" bpmnElement="Flow_18pax8n"> <bpmndi:BPMNEdge id="Flow_18pax8n_di" bpmnElement="Flow_18pax8n">
<di:waypoint x="740" y="560" /> <di:waypoint x="650" y="560" />
<di:waypoint x="800" y="560" /> <di:waypoint x="710" y="560" />
<di:waypoint x="800" y="464" /> <di:waypoint x="710" y="464" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0lrz4jq_di" bpmnElement="Flow_0lrz4jq"> <bpmndi:BPMNEdge id="Flow_0lrz4jq_di" bpmnElement="Flow_0lrz4jq">
<di:waypoint x="583" y="464" /> <di:waypoint x="493" y="464" />
<di:waypoint x="583" y="560" /> <di:waypoint x="493" y="560" />
<di:waypoint x="640" y="560" /> <di:waypoint x="550" y="560" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1bdr0gi_di" bpmnElement="Flow_1bdr0gi"> <bpmndi:BPMNEdge id="Flow_1bdr0gi_di" bpmnElement="Flow_1bdr0gi">
<di:waypoint x="740" y="439" /> <di:waypoint x="650" y="439" />
<di:waypoint x="775" y="439" /> <di:waypoint x="685" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tgxyp5_di" bpmnElement="Flow_1tgxyp5"> <bpmndi:BPMNEdge id="Flow_1tgxyp5_di" bpmnElement="Flow_1tgxyp5">
<di:waypoint x="608" y="439" /> <di:waypoint x="518" y="439" />
<di:waypoint x="640" y="439" /> <di:waypoint x="550" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0x9580l_di" bpmnElement="Flow_0x9580l"> <bpmndi:BPMNEdge id="Flow_0x9580l_di" bpmnElement="Flow_0x9580l">
<di:waypoint x="740" y="690" /> <di:waypoint x="650" y="690" />
<di:waypoint x="800" y="690" /> <di:waypoint x="710" y="690" />
<di:waypoint x="800" y="464" /> <di:waypoint x="710" y="464" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18pl92p_di" bpmnElement="Flow_18pl92p"> <bpmndi:BPMNEdge id="Flow_18pl92p_di" bpmnElement="Flow_18pl92p">
<di:waypoint x="583" y="464" /> <di:waypoint x="493" y="464" />
<di:waypoint x="583" y="690" /> <di:waypoint x="493" y="690" />
<di:waypoint x="640" y="690" /> <di:waypoint x="550" y="690" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_17ct47v_di" bpmnElement="SequenceFlow_17ct47v"> <bpmndi:BPMNEdge id="SequenceFlow_17ct47v_di" bpmnElement="SequenceFlow_17ct47v">
<di:waypoint x="500" y="439" /> <di:waypoint x="410" y="439" />
<di:waypoint x="558" y="439" /> <di:waypoint x="468" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1m8285h_di" bpmnElement="Flow_1m8285h"> <bpmndi:BPMNEdge id="Flow_1m8285h_di" bpmnElement="Flow_1m8285h">
<di:waypoint x="583" y="414" /> <di:waypoint x="493" y="414" />
<di:waypoint x="583" y="300" /> <di:waypoint x="493" y="300" />
<di:waypoint x="640" y="300" /> <di:waypoint x="550" y="300" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0pwtiqm_di" bpmnElement="Flow_0pwtiqm"> <bpmndi:BPMNEdge id="Flow_0pwtiqm_di" bpmnElement="Flow_0pwtiqm">
<di:waypoint x="825" y="439" /> <di:waypoint x="735" y="439" />
<di:waypoint x="880" y="439" /> <di:waypoint x="790" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1sggkit_di" bpmnElement="Flow_1sggkit"> <bpmndi:BPMNEdge id="Flow_1sggkit_di" bpmnElement="Flow_1sggkit">
<di:waypoint x="740" y="300" /> <di:waypoint x="650" y="300" />
<di:waypoint x="800" y="300" /> <di:waypoint x="710" y="300" />
<di:waypoint x="800" y="414" /> <di:waypoint x="710" y="414" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1ees8ka_di" bpmnElement="SequenceFlow_1ees8ka"> <bpmndi:BPMNEdge id="SequenceFlow_1ees8ka_di" bpmnElement="SequenceFlow_1ees8ka">
<di:waypoint x="318" y="439" /> <di:waypoint x="228" y="439" />
<di:waypoint x="400" y="439" /> <di:waypoint x="310" y="439" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="282" y="421" width="36" height="36" /> <dc:Bounds x="192" y="421" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_135x8jg_di" bpmnElement="Event_135x8jg"> <bpmndi:BPMNShape id="Event_135x8jg_di" bpmnElement="Event_135x8jg">
<dc:Bounds x="1712" y="421" width="36" height="36" /> <dc:Bounds x="1622" y="421" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_0x4a3pe_di" bpmnElement="Task_Load_Requirements"> <bpmndi:BPMNShape id="ScriptTask_0x4a3pe_di" bpmnElement="Task_Load_Requirements">
<dc:Bounds x="400" y="399" width="100" height="80" /> <dc:Bounds x="310" y="399" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1yqy50i_di" bpmnElement="Activity_1yqy50i"> <bpmndi:BPMNShape id="Activity_1yqy50i_di" bpmnElement="Activity_1yqy50i">
<dc:Bounds x="640" y="260" width="100" height="80" /> <dc:Bounds x="550" y="260" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1kk6x70_di" bpmnElement="Gateway_12tpgcy"> <bpmndi:BPMNShape id="Gateway_1kk6x70_di" bpmnElement="Gateway_12tpgcy">
<dc:Bounds x="775" y="414" width="50" height="50" /> <dc:Bounds x="685" y="414" width="50" height="50" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1m22g4p_di" bpmnElement="Gateway_1nta7st"> <bpmndi:BPMNShape id="Gateway_1m22g4p_di" bpmnElement="Gateway_1nta7st">
<dc:Bounds x="558" y="414" width="50" height="50" /> <dc:Bounds x="468" y="414" width="50" height="50" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_16cm213_di" bpmnElement="Activity_16cm213"> <bpmndi:BPMNShape id="Activity_16cm213_di" bpmnElement="Activity_16cm213">
<dc:Bounds x="640" y="650" width="100" height="80" /> <dc:Bounds x="550" y="650" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zpnt48_di" bpmnElement="Activity_1bqc7fa"> <bpmndi:BPMNShape id="Activity_0zpnt48_di" bpmnElement="Activity_1bqc7fa">
<dc:Bounds x="640" y="399" width="100" height="80" /> <dc:Bounds x="550" y="399" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0cxqj36_di" bpmnElement="Activity_0a14ftj"> <bpmndi:BPMNShape id="Activity_0cxqj36_di" bpmnElement="Activity_0a14ftj">
<dc:Bounds x="640" y="520" width="100" height="80" /> <dc:Bounds x="550" y="520" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1hkeo8n_di" bpmnElement="Activity_0f295la"> <bpmndi:BPMNShape id="Activity_1hkeo8n_di" bpmnElement="Activity_0f295la">
<dc:Bounds x="880" y="399" width="100" height="80" /> <dc:Bounds x="790" y="399" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1u4ccm9_di" bpmnElement="Activity_0ahlc3u"> <bpmndi:BPMNShape id="Activity_1u4ccm9_di" bpmnElement="Activity_0ahlc3u">
<dc:Bounds x="1120" y="490" width="100" height="80" /> <dc:Bounds x="1030" y="490" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0qkzul9_di" bpmnElement="Activity_0teqy3w"> <bpmndi:BPMNShape id="Activity_0qkzul9_di" bpmnElement="Activity_0teqy3w">
<dc:Bounds x="1110" y="320" width="100" height="80" /> <dc:Bounds x="1020" y="320" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1y87e9r_di" bpmnElement="Gateway_1so972f"> <bpmndi:BPMNShape id="Gateway_1y87e9r_di" bpmnElement="Gateway_1so972f">
<dc:Bounds x="1015" y="414" width="50" height="50" /> <dc:Bounds x="925" y="414" width="50" height="50" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1c142bm_di" bpmnElement="Gateway_15ksf70"> <bpmndi:BPMNShape id="Gateway_1c142bm_di" bpmnElement="Gateway_15ksf70">
<dc:Bounds x="1255" y="414" width="50" height="50" /> <dc:Bounds x="1165" y="414" width="50" height="50" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1s8l694_di" bpmnElement="Activity_0g3qa1c"> <bpmndi:BPMNShape id="Activity_1s8l694_di" bpmnElement="Activity_0g3qa1c">
<dc:Bounds x="1360" y="399" width="100" height="80" /> <dc:Bounds x="1270" y="399" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1onpeul_di" bpmnElement="Activity_13ep6ar"> <bpmndi:BPMNShape id="Activity_1onpeul_di" bpmnElement="Activity_13ep6ar">
<dc:Bounds x="1530" y="399" width="100" height="80" /> <dc:Bounds x="1440" y="399" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1pv8ygy_di" bpmnElement="TextAnnotation_1pv8ygy"> <bpmndi:BPMNShape id="TextAnnotation_1pv8ygy_di" bpmnElement="TextAnnotation_1pv8ygy">
<dc:Bounds x="400" y="247" width="100" height="68" /> <dc:Bounds x="310" y="247" width="100" height="68" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_0ydnva4_di" bpmnElement="TextAnnotation_0ydnva4"> <bpmndi:BPMNShape id="TextAnnotation_0ydnva4_di" bpmnElement="TextAnnotation_0ydnva4">
<dc:Bounds x="245" y="210" width="110" height="82" /> <dc:Bounds x="155" y="210" width="110" height="82" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1f52jro_di" bpmnElement="TextAnnotation_1f52jro"> <bpmndi:BPMNShape id="TextAnnotation_1f52jro_di" bpmnElement="TextAnnotation_1f52jro">
<dc:Bounds x="461" y="80" width="243" height="124" /> <dc:Bounds x="371" y="80" width="243" height="124" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_0w69z3w_di" bpmnElement="Association_0w69z3w"> <bpmndi:BPMNEdge id="Association_0w69z3w_di" bpmnElement="Association_0w69z3w">
<di:waypoint x="450" y="399" /> <di:waypoint x="360" y="399" />
<di:waypoint x="450" y="315" /> <di:waypoint x="360" y="315" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_0a41ixa_di" bpmnElement="Association_0a41ixa"> <bpmndi:BPMNEdge id="Association_0a41ixa_di" bpmnElement="Association_0a41ixa">
<di:waypoint x="300" y="421" /> <di:waypoint x="210" y="421" />
<di:waypoint x="300" y="292" /> <di:waypoint x="210" y="292" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1mzqzwj_di" bpmnElement="Association_1mzqzwj"> <bpmndi:BPMNEdge id="Association_1mzqzwj_di" bpmnElement="Association_1mzqzwj">
<di:waypoint x="583" y="414" /> <di:waypoint x="493" y="414" />
<di:waypoint x="583" y="204" /> <di:waypoint x="493" y="204" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>

View File

@ -7,7 +7,7 @@
<decisionTable id="DecisionTable_1mjqwlv"> <decisionTable id="DecisionTable_1mjqwlv">
<input id="InputClause_18pwfqu" label="Data Plan Required in PB?"> <input id="InputClause_18pwfqu" label="Data Plan Required in PB?">
<inputExpression id="LiteralExpression_1y84stb" typeRef="boolean" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1y84stb" typeRef="boolean" expressionLanguage="feel">
<text>Documents['Study_DataSecurityPlan']['required']</text> <text>StudyInfo.documents.Study_DataSecurityPlan.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" /> <output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" />

View File

@ -7,7 +7,7 @@
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="InputClause_1ki80j6" label="required doc ids"> <input id="InputClause_1ki80j6" label="required doc ids">
<inputExpression id="LiteralExpression_10mfcy7" typeRef="boolean" expressionLanguage="Python"> <inputExpression id="LiteralExpression_10mfcy7" typeRef="boolean" expressionLanguage="Python">
<text>Documents['UVACompl_PRCAppr']['required']</text> <text>StudyInfo.documents.UVACompl_PRCAppr.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" /> <output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" />

View File

@ -7,7 +7,7 @@
<decisionTable id="DecisionTable_00zdxg0"> <decisionTable id="DecisionTable_00zdxg0">
<input id="InputClause_02n3ccs" label="Sponsor Document Required in PB?"> <input id="InputClause_02n3ccs" label="Sponsor Document Required in PB?">
<inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel">
<text>Documents['AD_LabManual']['required']</text> <text>StudyInfo.documents.AD_LabManual.required</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_1ybi1ud" label="Sponsor Funding Source" name="sponsor_funding_source" typeRef="string" /> <output id="OutputClause_1ybi1ud" label="Sponsor Funding Source" name="sponsor_funding_source" typeRef="string" />

View File

@ -11,7 +11,7 @@
<bpmn:scriptTask id="Task_Load_Requirements" name="Load Required Documents From PM"> <bpmn:scriptTask id="Task_Load_Requirements" name="Load Required Documents From PM">
<bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming> <bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing> <bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>Documents</bpmn:script> <bpmn:script>StudyInfo documents</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info"> <bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<bpmn:incoming>Flow_1m8285h</bpmn:incoming> <bpmn:incoming>Flow_1m8285h</bpmn:incoming>

View File

@ -1,82 +0,0 @@
import json
from unittest.mock import patch
from crc import db
from crc.models.file import FileDataModel, FileModel
from crc.models.protocol_builder import ProtocolBuilderRequiredDocumentSchema
from crc.scripts.documents import Documents
from crc.services.file_service import FileService
from tests.base_test import BaseTest
class TestRequiredDocsScript(BaseTest):
test_uid = "dhf8r"
test_study_id = 1
"""
1. get a list of only the required documents for the study.
2. For this study, is this document required accroding to the protocol builder?
3. For ALL uploaded documents, what the total number of files that were uploaded? per instance of this document naming
convention that we are implementing for the IRB.
"""
def test_validate_returns_error_if_reference_files_do_not_exist(self):
file_model = db.session.query(FileModel). \
filter(FileModel.is_reference == True). \
filter(FileModel.name == FileService.IRB_PRO_CATEGORIES_FILE).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()
db.session.commit()
db.session.flush()
errors = Documents.validate()
self.assertTrue(len(errors) > 0)
self.assertEqual("file_not_found", errors[0].code)
def test_no_validation_error_when_correct_file_exists(self):
self.create_reference_document()
errors = Documents.validate()
self.assertTrue(len(errors) == 0)
def test_load_lookup_data(self):
self.create_reference_document()
dict = FileService.get_file_reference_dictionary()
self.assertIsNotNone(dict)
def get_required_docs(self):
string_data = self.protocol_builder_response('required_docs.json')
return ProtocolBuilderRequiredDocumentSchema(many=True).loads(string_data)
def test_get_required_docs(self):
pb_docs = self.get_required_docs()
self.create_reference_document()
script = Documents()
documents = script.get_documents(12, pb_docs) # Mocked out, any random study id works.
self.assertIsNotNone(documents)
self.assertTrue("UVACompl_PRCAppr" in documents.keys())
self.assertEqual("Cancer Center's PRC Approval Form", documents["UVACompl_PRCAppr"]['Name'])
self.assertEqual("UVA Compliance", documents["UVACompl_PRCAppr"]['category1'])
self.assertEqual("PRC Approval", documents["UVACompl_PRCAppr"]['category2'])
self.assertEqual("CRC", documents["UVACompl_PRCAppr"]['Who Uploads?'])
self.assertEqual(0, documents["UVACompl_PRCAppr"]['count'])
self.assertEqual(True, documents["UVACompl_PRCAppr"]['required'])
self.assertEqual('6', documents["UVACompl_PRCAppr"]['Id'])
def test_get_required_docs_has_correct_count_when_a_file_exists(self):
self.load_example_data()
pb_docs = self.get_required_docs()
# Make sure the xslt reference document is in place.
self.create_reference_document()
script = Documents()
# Add a document to the study with the correct code.
workflow = self.create_workflow('docx')
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
task_id="fakingthisout",
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
docs = script.get_documents(workflow.study_id, pb_docs)
self.assertIsNotNone(docs)
self.assertEqual(1, docs["UVACompl_PRCAppr"]['count'])

View File

@ -0,0 +1,67 @@
import json
from unittest.mock import patch
from crc import db, session
from crc.api.common import ApiError
from crc.models.file import FileDataModel, FileModel
from crc.models.protocol_builder import ProtocolBuilderRequiredDocumentSchema
from crc.models.study import StudyModel
from crc.scripts.study_info import StudyInfo
from crc.services.file_service import FileService
from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor
from tests.base_test import BaseTest
class TestStudyDetailsDocumentsScript(BaseTest):
test_uid = "dhf8r"
test_study_id = 1
"""
1. get a list of all documents related to the study.
2. For this study, is this document required accroding to the protocol builder?
3. For ALL uploaded documents, what the total number of files that were uploaded? per instance of this document naming
convention that we are implementing for the IRB.
"""
def test_validate_returns_error_if_reference_files_do_not_exist(self):
self.load_example_data()
self.create_reference_document()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("two_forms")
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
processor = WorkflowProcessor(workflow_model)
task = processor.next_task()
# Remove the reference file.
file_model = db.session.query(FileModel). \
filter(FileModel.is_reference == True). \
filter(FileModel.name == FileService.IRB_PRO_CATEGORIES_FILE).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()
db.session.commit()
db.session.flush()
with self.assertRaises(ApiError):
StudyInfo().do_task_validate_only(task, study.id, "documents")
def test_no_validation_error_when_correct_file_exists(self):
self.load_example_data()
self.create_reference_document()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("two_forms")
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
processor = WorkflowProcessor(workflow_model)
task = processor.next_task()
StudyInfo().do_task_validate_only(task, study.id, "documents")
def test_load_lookup_data(self):
self.create_reference_document()
dict = FileService.get_file_reference_dictionary()
self.assertIsNotNone(dict)
def get_required_docs(self):
string_data = self.protocol_builder_response('required_docs.json')
return ProtocolBuilderRequiredDocumentSchema(many=True).loads(string_data)

View File

@ -8,6 +8,7 @@ from crc.models.study import StudyModel
from crc.models.user import UserModel from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel, WorkflowStatus, \ from crc.models.workflow import WorkflowModel, WorkflowStatus, \
WorkflowSpecCategoryModel WorkflowSpecCategoryModel
from crc.services.file_service import FileService
from crc.services.study_service import StudyService from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
from example_data import ExampleDataLoader from example_data import ExampleDataLoader
@ -17,22 +18,28 @@ from tests.base_test import BaseTest
class TestStudyService(BaseTest): class TestStudyService(BaseTest):
"""Largely tested via the test_study_api, and time is tight, but adding new tests here.""" """Largely tested via the test_study_api, and time is tight, but adding new tests here."""
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs def create_user_with_study_and_workflow(self):
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') # clear it all out.
mock_docs.return_value = json.loads(docs_response) from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
# Assure some basic models are in place, This is a damn mess. Our database models need an overhaul to make # 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. # this easier - better relationship modeling is now critical.
self.load_test_spec("top_level_workflow", master_spec=True) self.load_test_spec("top_level_workflow", master_spec=True)
user = db.session.query(UserModel).filter(UserModel.uid == "dhf8r").first()
if not user:
user = UserModel(uid="dhf8r", email_address="whatever@stuff.com", display_name="Stayathome Smellalots") user = UserModel(uid="dhf8r", email_address="whatever@stuff.com", display_name="Stayathome Smellalots")
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
else:
for study in db.session.query(StudyModel).all():
StudyService().delete_study(study.id)
study = StudyModel(title="My title", protocol_builder_status=ProtocolBuilderStatus.ACTIVE, user_uid=user.uid) study = StudyModel(title="My title", protocol_builder_status=ProtocolBuilderStatus.ACTIVE, user_uid=user.uid)
db.session.add(study)
cat = WorkflowSpecCategoryModel(name="approvals", display_name="Approvals", display_order=0) cat = WorkflowSpecCategoryModel(name="approvals", display_name="Approvals", display_order=0)
db.session.add_all([study, cat]) db.session.add(cat)
db.session.commit() db.session.commit()
self.assertIsNotNone(cat.id) self.assertIsNotNone(cat.id)
@ -45,6 +52,16 @@ class TestStudyService(BaseTest):
db.session.commit() db.session.commit()
# Assure there is a master specification, one standard spec, and lookup tables. # Assure there is a master specification, one standard spec, and lookup tables.
ExampleDataLoader().load_reference_documents() ExampleDataLoader().load_reference_documents()
return user
@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)
user = self.create_user_with_study_and_workflow()
# The load example data script should set us up a user and at least one study, one category, and one workflow. # The load example data script should set us up a user and at least one study, one category, and one workflow.
studies = StudyService.get_studies_for_user(user) studies = StudyService.get_studies_for_user(user)
@ -86,3 +103,63 @@ class TestStudyService(BaseTest):
approvals = StudyService.get_approvals(studies[0].id) approvals = StudyService.get_approvals(studies[0].id)
self.assertGreater(len(approvals), 0) self.assertGreater(len(approvals), 0)
self.assertIsNotNone(approvals[0]['display_order']) self.assertIsNotNone(approvals[0]['display_order'])
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
def test_get_required_docs(self, mock_docs):
# mock out the protocol builder
docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response)
user = self.create_user_with_study_and_workflow()
studies = StudyService.get_studies_for_user(user)
study = studies[0]
study_service = StudyService()
documents = study_service.get_documents_status(study_id=study.id) # Mocked out, any random study id works.
self.assertIsNotNone(documents)
self.assertTrue("UVACompl_PRCAppr" in documents.keys())
self.assertEqual("UVACompl_PRCAppr", documents["UVACompl_PRCAppr"]['code'])
self.assertEqual("UVA Compliance / PRC Approval", documents["UVACompl_PRCAppr"]['display_name'])
self.assertEqual("Cancer Center's PRC Approval Form", documents["UVACompl_PRCAppr"]['description'])
self.assertEqual("UVA Compliance", documents["UVACompl_PRCAppr"]['category1'])
self.assertEqual("PRC Approval", documents["UVACompl_PRCAppr"]['category2'])
self.assertEqual("", documents["UVACompl_PRCAppr"]['category3'])
self.assertEqual("CRC", documents["UVACompl_PRCAppr"]['Who Uploads?'])
self.assertEqual(0, documents["UVACompl_PRCAppr"]['count'])
self.assertEqual(True, documents["UVACompl_PRCAppr"]['required'])
self.assertEqual('6', documents["UVACompl_PRCAppr"]['id'])
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
def test_get_documents_has_file_details(self, mock_docs):
# mock out the protocol builder
docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response)
user = self.create_user_with_study_and_workflow()
# Add a document to the study with the correct code.
workflow = self.create_workflow('docx')
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
workflow_spec_id=workflow.workflow_spec_id,
task_id="fakingthisout",
name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code)
docs = StudyService().get_documents_status(workflow.study_id)
self.assertIsNotNone(docs)
self.assertEqual("not_started", docs["UVACompl_PRCAppr"]['status'])
self.assertEqual(1, docs["UVACompl_PRCAppr"]['count'])
self.assertIsNotNone(docs["UVACompl_PRCAppr"]['files'][0])
self.assertIsNotNone(docs["UVACompl_PRCAppr"]['files'][0]['file_id'])
self.assertEquals(workflow.id, docs["UVACompl_PRCAppr"]['files'][0]['workflow_id'])
self.assertEquals(workflow.workflow_spec_id, docs["UVACompl_PRCAppr"]['files'][0]['workflow_spec_id'])
# 'file_id': 123,
# 'task_id': 'abcdef14236890',
# 'workflow_id': 456,
# 'workflow_spec_id': 'irb_api_details',
# 'status': 'complete',