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.api.common import ApiError
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
import jinja2
@ -33,11 +33,12 @@ Takes two arguments:
def do_task(self, task, study_id, *args, **kwargs):
workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
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]
irb_doc_code = args[1]
FileService.add_task_file(study_id=study_id,
workflow_id=workflow_id,
workflow_spec_id=workflow.workflow_spec_id,
task_id=task.id,
name=file_name,
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, " +
"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
def get_all_subclasses():
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.models.study import StudyModel, StudySchema
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.protocol_builder import ProtocolBuilderService
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."""
pb = ProtocolBuilderService()
type_options = ['info', 'investigators', 'details', 'approvals', 'documents_status']
type_options = ['info', 'investigators', 'details', 'approvals', 'documents', 'protocol']
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
provided as an argument. Basic returns the basic information such as the title. Investigators provides
detailed information about each investigator in th study. Details provides a large number
of details about the study, as gathered within the protocol builder, and 'required_docs',
lists all the documents the Protocol Builder has determined will be required as a part of
this study.
provided as an argument. 'info' returns the basic information such as the title. 'Investigators' provides
detailed information about each investigator in th study. 'Details' provides a large number
of details about the study, as gathered within the protocol builder, and 'documents',
lists all the documents that can be a part of the study, with documents from Protocol Builder
marked as required, and details about any files that were uploaded' .
"""
def do_task_validate_only(self, task, study_id, *args, **kwargs):
"""For validation only, pretend no results come back from pb"""
self.check_args(args)
# Assure the reference file exists (a bit hacky, but we want to raise this error early, and cleanly.)
FileService.get_file_reference_dictionary()
data = {
"study":{
"info": {
@ -57,7 +61,8 @@ class StudyInfo(Script):
"status": WorkflowStatus.not_started.value,
"workflow_spec_id": "irb_api_details",
},
"documents_status": [
"documents": {
"AD_CoCApp":
{
'category1': 'Ancillary Document',
'category2': 'CoC Application',
@ -75,7 +80,10 @@ class StudyInfo(Script):
'workflow_spec_id': 'irb_api_details',
'status': 'complete',
}
]
},
'protocol': {
id: 0,
}
}
}
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)})
if cmd == 'approvals':
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)})
if cmd == 'protocol':
self.add_data_to_task(task, {cmd: StudyService().get_protocol(study_id)})
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)
xls = ExcelFile(data_model.data)
df = xls.parse(xls.sheet_names[0])
return code in df['Code'].values
return code in df['code'].values
@staticmethod
def get_file_reference_dictionary():
@ -65,11 +65,11 @@ class FileService(object):
data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE)
xls = ExcelFile(data_model.data)
df = xls.parse(xls.sheet_names[0])
df['Id'] = df['Id'].fillna(0)
df = df.astype({'Id': 'Int64'})
df['id'] = df['id'].fillna(0)
df = df.astype({'id': 'Int64'})
df = df.fillna('')
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.
# df.columns = [snakeCase(x) for x in df.columns]
return json.loads(df.to_json(orient='index'))
@ -80,12 +80,13 @@ class FileService(object):
# all_dict = df.set_index('Id').to_dict('index')
@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):
"""Create a new file and associate it with an executing task within a workflow."""
file_model = FileModel(
study_id=study_id,
workflow_id=workflow_id,
workflow_spec_id=workflow_spec_id,
task_id=task_id,
name=name,
irb_doc_code=irb_doc_code
@ -168,7 +169,7 @@ class FileService(object):
if form_field_key:
query = query.filter_by(form_field_key=form_field_key)
if name:
query = query.filter_by(name=form_field_key)
query = query.filter_by(name=name)
if irb_doc_code:
query = query.filter_by(irb_doc_code=irb_doc_code)

View File

@ -41,14 +41,11 @@ class ProtocolBuilderService(object):
(response.status_code, response.text))
@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)
response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id)
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:
raise ApiError("protocol_builder_error",
"Received an invalid response from the protocol builder (status %s): %s" %

View File

@ -1,17 +1,17 @@
from datetime import datetime
import json
from typing import List
from SpiffWorkflow import WorkflowException
from crc import db, session
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.stats import TaskEventModel
from crc.models.study import StudyModel, Study, Category, WorkflowMetadata
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus
from crc.scripts.documents import Documents
from crc.services.file_service import FileService
from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.workflow_processor import WorkflowProcessor
@ -39,6 +39,9 @@ class StudyService(object):
study = Study.from_model(study_model)
study.categories = StudyService.get_categories()
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)
study.warnings = StudyService.__update_status_of_workflow_meta(workflow_metas, status)
@ -101,52 +104,63 @@ class StudyService(object):
@staticmethod
def get_documents_status(study_id):
"""Returns a list of required documents and related workflow status."""
doc_service = Documents()
"""Returns a list of documents related to the study, if they are required, and any file information
that is available.."""
# 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
study_docs = doc_service.get_documents(study_id=study_id, pb_docs=pb_docs)
# Container for results
documents = []
# For each required doc, get file(s)
for code, doc in study_docs.items():
if not doc['required']:
continue
# Loop through all known document types, get the counts for those files, and use pb_docs to mark those required.
doc_dictionary = FileService.get_file_reference_dictionary()
documents = {}
for code, doc in doc_dictionary.items():
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['code'] = code
# Make a display name out of categories if none exists
if 'Name' in doc and len(doc['Name']) > 0:
doc['display_name'] = doc['Name']
else:
name_list = []
for cat_key in ['category1', 'category2', 'category3']:
if doc[cat_key] not in ['', 'NULL']:
name_list.append(doc[cat_key])
doc['display_name'] = ' '.join(name_list)
# Make a display name out of categories
name_list = []
for cat_key in ['category1', 'category2', 'category3']:
if doc[cat_key] not in ['', 'NULL']:
name_list.append(doc[cat_key])
doc['display_name'] = ' / '.join(name_list)
# For each file, get associated workflow status
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:
doc['file_id'] = file.id
doc['task_id'] = file.task_id
doc['workflow_id'] = file.workflow_id
doc['workflow_spec_id'] = file.workflow_spec_id
doc['files'].append({'file_id': file.id,
'task_id': file.task_id,
'workflow_id': file.workflow_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()
doc['status'] = workflow.status.value
documents.append(doc)
documents[code] = doc
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
def synch_all_studies_with_protocol_builder(user):
"""Assures that the studies we have locally for the given user are

View File

@ -13,23 +13,29 @@
<bpmn:documentation># Documents &amp; Approvals
&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; | Name | Status | Help |
|:---- |:------ |:---- |
{% 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 %}
&gt; ## Documents
&gt; | Name | Status | Help | Download |
|:---- |:------ |:---- |:-------- |
{% for doc in StudyInfo.documents_status -%}
{% for doc in StudyInfo.documents -%}
{% 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 -%}
| {{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 %}
{% endfor %}</bpmn:documentation>
<bpmn:extensionElements>
@ -46,47 +52,60 @@
<bpmn:script>StudyInfo approvals</bpmn:script>
</bpmn:scriptTask>
<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:script>StudyInfo documents_status</bpmn:script>
</bpmn:scriptTask>
<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_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>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1gmf4la">
<bpmndi:BPMNEdge id="Flow_1k3su2q_di" bpmnElement="Flow_1k3su2q">
<di:waypoint x="228" y="117" />
<di:waypoint x="290" y="117" />
<di:waypoint x="188" y="117" />
<di:waypoint x="240" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c7ryff_di" bpmnElement="Flow_0c7ryff">
<di:waypoint x="390" y="117" />
<di:waypoint x="440" y="117" />
<di:waypoint x="490" y="117" />
<di:waypoint x="540" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_142jtxs_di" bpmnElement="Flow_142jtxs">
<di:waypoint x="540" y="117" />
<di:waypoint x="610" y="117" />
<di:waypoint x="640" y="117" />
<di:waypoint x="710" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0m7unlb_di" bpmnElement="Flow_0m7unlb">
<di:waypoint x="710" y="117" />
<di:waypoint x="782" y="117" />
<di:waypoint x="810" y="117" />
<di:waypoint x="882" y="117" />
</bpmndi:BPMNEdge>
<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 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 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 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 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 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:BPMNDiagram>
</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:incoming>SequenceFlow_1dexemq</bpmn:incoming>
<bpmn:outgoing>Flow_1x9d2mo</bpmn:outgoing>
<bpmn:script>Documents</bpmn:script>
<bpmn:script>StudyInfo documents</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
<decisionTable id="DecisionTable_00zdxg0">
<input id="InputClause_02n3ccs" label="CoCApplication Required?">
<inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel">
<text>Documents['AD_LabManual']['required']</text>
<text>StudyInfo.documents.AD_LabManual.required</text>
</inputExpression>
</input>
<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:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>Documents</bpmn:script>
<bpmn:script>StudyInfo documents</bpmn:script>
</bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<bpmn:incoming>Flow_1m8285h</bpmn:incoming>
@ -109,159 +109,159 @@
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0jhpidf">
<bpmndi:BPMNEdge id="Flow_1ybicki_di" bpmnElement="Flow_1ybicki">
<di:waypoint x="1630" y="439" />
<di:waypoint x="1712" y="439" />
<di:waypoint x="1540" y="439" />
<di:waypoint x="1622" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vo6ul1_di" bpmnElement="Flow_0vo6ul1">
<di:waypoint x="1460" y="439" />
<di:waypoint x="1530" y="439" />
<di:waypoint x="1370" y="439" />
<di:waypoint x="1440" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1qyrmzn_di" bpmnElement="Flow_1qyrmzn">
<di:waypoint x="1305" y="439" />
<di:waypoint x="1360" y="439" />
<di:waypoint x="1215" y="439" />
<di:waypoint x="1270" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ffvg2f_di" bpmnElement="Flow_0ffvg2f">
<di:waypoint x="1220" y="530" />
<di:waypoint x="1280" y="530" />
<di:waypoint x="1280" y="464" />
<di:waypoint x="1130" y="530" />
<di:waypoint x="1190" y="530" />
<di:waypoint x="1190" y="464" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_145qxh8_di" bpmnElement="Flow_145qxh8">
<di:waypoint x="1210" y="360" />
<di:waypoint x="1280" y="360" />
<di:waypoint x="1280" y="414" />
<di:waypoint x="1120" y="360" />
<di:waypoint x="1190" y="360" />
<di:waypoint x="1190" y="414" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1jini69_di" bpmnElement="Flow_1jini69">
<di:waypoint x="1040" y="414" />
<di:waypoint x="1040" y="360" />
<di:waypoint x="1110" y="360" />
<di:waypoint x="950" y="414" />
<di:waypoint x="950" y="360" />
<di:waypoint x="1020" y="360" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_14ce1d7_di" bpmnElement="Flow_14ce1d7">
<di:waypoint x="1040" y="464" />
<di:waypoint x="1040" y="530" />
<di:waypoint x="1120" y="530" />
<di:waypoint x="950" y="464" />
<di:waypoint x="950" y="530" />
<di:waypoint x="1030" y="530" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0eq6px2_di" bpmnElement="Flow_0eq6px2">
<di:waypoint x="980" y="439" />
<di:waypoint x="1015" y="439" />
<di:waypoint x="890" y="439" />
<di:waypoint x="925" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18pax8n_di" bpmnElement="Flow_18pax8n">
<di:waypoint x="740" y="560" />
<di:waypoint x="800" y="560" />
<di:waypoint x="800" y="464" />
<di:waypoint x="650" y="560" />
<di:waypoint x="710" y="560" />
<di:waypoint x="710" y="464" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0lrz4jq_di" bpmnElement="Flow_0lrz4jq">
<di:waypoint x="583" y="464" />
<di:waypoint x="583" y="560" />
<di:waypoint x="640" y="560" />
<di:waypoint x="493" y="464" />
<di:waypoint x="493" y="560" />
<di:waypoint x="550" y="560" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1bdr0gi_di" bpmnElement="Flow_1bdr0gi">
<di:waypoint x="740" y="439" />
<di:waypoint x="775" y="439" />
<di:waypoint x="650" y="439" />
<di:waypoint x="685" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tgxyp5_di" bpmnElement="Flow_1tgxyp5">
<di:waypoint x="608" y="439" />
<di:waypoint x="640" y="439" />
<di:waypoint x="518" y="439" />
<di:waypoint x="550" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0x9580l_di" bpmnElement="Flow_0x9580l">
<di:waypoint x="740" y="690" />
<di:waypoint x="800" y="690" />
<di:waypoint x="800" y="464" />
<di:waypoint x="650" y="690" />
<di:waypoint x="710" y="690" />
<di:waypoint x="710" y="464" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_18pl92p_di" bpmnElement="Flow_18pl92p">
<di:waypoint x="583" y="464" />
<di:waypoint x="583" y="690" />
<di:waypoint x="640" y="690" />
<di:waypoint x="493" y="464" />
<di:waypoint x="493" y="690" />
<di:waypoint x="550" y="690" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_17ct47v_di" bpmnElement="SequenceFlow_17ct47v">
<di:waypoint x="500" y="439" />
<di:waypoint x="558" y="439" />
<di:waypoint x="410" y="439" />
<di:waypoint x="468" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1m8285h_di" bpmnElement="Flow_1m8285h">
<di:waypoint x="583" y="414" />
<di:waypoint x="583" y="300" />
<di:waypoint x="640" y="300" />
<di:waypoint x="493" y="414" />
<di:waypoint x="493" y="300" />
<di:waypoint x="550" y="300" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0pwtiqm_di" bpmnElement="Flow_0pwtiqm">
<di:waypoint x="825" y="439" />
<di:waypoint x="880" y="439" />
<di:waypoint x="735" y="439" />
<di:waypoint x="790" y="439" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1sggkit_di" bpmnElement="Flow_1sggkit">
<di:waypoint x="740" y="300" />
<di:waypoint x="800" y="300" />
<di:waypoint x="800" y="414" />
<di:waypoint x="650" y="300" />
<di:waypoint x="710" y="300" />
<di:waypoint x="710" y="414" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1ees8ka_di" bpmnElement="SequenceFlow_1ees8ka">
<di:waypoint x="318" y="439" />
<di:waypoint x="400" y="439" />
<di:waypoint x="228" y="439" />
<di:waypoint x="310" y="439" />
</bpmndi:BPMNEdge>
<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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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:BPMNEdge id="Association_0w69z3w_di" bpmnElement="Association_0w69z3w">
<di:waypoint x="450" y="399" />
<di:waypoint x="450" y="315" />
<di:waypoint x="360" y="399" />
<di:waypoint x="360" y="315" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_0a41ixa_di" bpmnElement="Association_0a41ixa">
<di:waypoint x="300" y="421" />
<di:waypoint x="300" y="292" />
<di:waypoint x="210" y="421" />
<di:waypoint x="210" y="292" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Association_1mzqzwj_di" bpmnElement="Association_1mzqzwj">
<di:waypoint x="583" y="414" />
<di:waypoint x="583" y="204" />
<di:waypoint x="493" y="414" />
<di:waypoint x="493" y="204" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>

View File

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

View File

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

View File

@ -7,7 +7,7 @@
<decisionTable id="DecisionTable_00zdxg0">
<input id="InputClause_02n3ccs" label="Sponsor Document Required in PB?">
<inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel">
<text>Documents['AD_LabManual']['required']</text>
<text>StudyInfo.documents.AD_LabManual.required</text>
</inputExpression>
</input>
<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:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>Documents</bpmn:script>
<bpmn:script>StudyInfo documents</bpmn:script>
</bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<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.workflow import WorkflowModel, WorkflowStatus, \
WorkflowSpecCategoryModel
from crc.services.file_service import FileService
from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor
from example_data import ExampleDataLoader
@ -17,22 +18,28 @@ 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."""
@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."""
def create_user_with_study_and_workflow(self):
docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response)
# clear it all out.
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
# this easier - better relationship modeling is now critical.
self.load_test_spec("top_level_workflow", master_spec=True)
user = UserModel(uid="dhf8r", email_address="whatever@stuff.com", display_name="Stayathome Smellalots")
db.session.add(user)
db.session.commit()
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")
db.session.add(user)
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)
db.session.add(study)
cat = WorkflowSpecCategoryModel(name="approvals", display_name="Approvals", display_order=0)
db.session.add_all([study, cat])
db.session.add(cat)
db.session.commit()
self.assertIsNotNone(cat.id)
@ -45,6 +52,16 @@ class TestStudyService(BaseTest):
db.session.commit()
# Assure there is a master specification, one standard spec, and lookup tables.
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.
studies = StudyService.get_studies_for_user(user)
@ -86,3 +103,63 @@ class TestStudyService(BaseTest):
approvals = StudyService.get_approvals(studies[0].id)
self.assertGreater(len(approvals), 0)
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',