Merge remote-tracking branch 'origin/master'

This commit is contained in:
Aaron Louie 2020-03-03 15:46:24 -05:00
commit 93168482c2
27 changed files with 287 additions and 180 deletions

View File

@ -680,7 +680,21 @@ paths:
type: string
format: binary
example: '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>'
/list_scripts:
get:
operationId: crc.api.tools.list_scripts
summary: Returns an list of scripts, along with their descriptions
tags:
- Configurator Tools
responses:
'201':
description: The list of scripts
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Script"
components:
schemas:
User:
@ -969,3 +983,12 @@ components:
message:
type: string
example: "You do not have permission to view the requested study."
Script:
properties:
name:
type: string
format: string
example: "random_fact"
description:
type: string
example: "Returns a random fact about a topic. Provide an argument of either 'cat', 'norris', or 'buzzword'"

View File

@ -6,6 +6,7 @@ class ApiError(Exception):
self.status_code = status_code
self.code = code
self.message = message
Exception.__init__(self, self.message)
class ApiErrorSchema(ma.Schema):

View File

@ -1,18 +1,14 @@
import io
import json
import uuid
from io import BytesIO
import connexion
import jinja2
from docxtpl import DocxTemplate
from flask import send_file
from jinja2 import Template, UndefinedError
from crc.api.common import ApiError, ApiErrorSchema
from crc.scripts.CompleteTemplate import CompleteTemplate
from crc.services.file_service import FileService
from crc.api.common import ApiError
from crc.scripts.complete_template import CompleteTemplate
from crc.scripts.script import Script
import crc.scripts
def render_markdown(data, template):
"""
@ -48,3 +44,17 @@ def render_docx(data):
raise ApiError(code="invalid", message=str(e))
except Exception as e:
raise ApiError(code="invalid", message=str(e))
def list_scripts():
"""Provides a list of scripts that can be called by a script task."""
scripts = Script.get_all_subclasses()
script_meta = []
for script_class in scripts:
script_meta.append(
{
"name": script_class.__name__,
"description": script_class().get_description()
})
return script_meta

View File

@ -1,9 +0,0 @@
import requests
class IRBDocAPI:
"""Just your basic class that can pull in data from a few api endpoints and do a basic task."""
def do_task(self, task_data):
print('*** IRB_Doc_API > do_task ***')
print('task_data', task_data)

View File

@ -1,24 +0,0 @@
import requests
class LoadStudies:
"""Just your basic class that can pull in data from a few api endpoints and do a basic task."""
def do_task(self, task_data):
print('*** LoadStudies > do_task ***')
print('task_data', task_data)
class LoadStudy:
"""Just your basic class that can pull in data from a few api endpoints and do a basic task."""
def do_task(self, task_data):
print('*** LoadStudies > do_task ***')
print('task_data', task_data)
irb_study = {
'tbd': 0,
'protocol_builder_available': True,
'irb_review_type': 'Full Board',
'irb_requires': True,
}
return irb_study

View File

@ -9,17 +9,18 @@ from crc.models.workflow import WorkflowSpecModel
from docxtpl import DocxTemplate
import jinja2
from crc.scripts.script import Script
from crc.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor
class CompleteTemplate(object):
"""Completes a word template, using the data available in the current task. Heavy on the
error messages, because there is so much that can go wrong here, and we want to provide
as much feedback as possible. Some of this might move up to a higher level object or be
passed into all tasks as we complete more work."""
class CompleteTemplate(Script):
def do_task(self, task, *args, **kwargs):
def get_description(self):
return """Takes one argument, which is the name of a MS Word docx file to use as a template.
All data currently collected up to this Task will be available for use in the template."""
def do_task(self, task, study_id, *args, **kwargs):
"""Entry point, mostly worried about wiring it all up."""
if len(args) != 1:
raise ApiError(code="missing_argument",

View File

@ -1,8 +1,12 @@
import requests
from crc.scripts.script import Script
class FactService:
"""Just your basic class that can pull in data from a few api endpoints and do a basic task."""
class FactService(Script):
def get_description(self):
return """Just your basic class that can pull in data from a few api endpoints and
do a basic task."""
def get_cat(self):
response = requests.get('https://cat-fact.herokuapp.com/facts/random')
@ -16,7 +20,7 @@ class FactService:
response = requests.get('https://api.chucknorris.io/jokes/random')
return response.json()['value']
def do_task(self, task, **kwargs):
def do_task(self, task, study_id, **kwargs):
print(task.data)
if "type" not in task.data:

41
crc/scripts/script.py Normal file
View File

@ -0,0 +1,41 @@
import importlib
import os
import pkgutil
from crc.api.common import ApiError
class Script:
""" Provides an abstract class that defines how scripts should work, this
must be extended in all Script Tasks."""
def get_description(self):
raise ApiError("invalid_script",
"This script does not supply a description.")
def do_task(self, task, study_id, **kwargs):
raise ApiError("invalid_script",
"This is an internal error. The script you are trying to execute " +
"does not properly implement the do_task function.")
@staticmethod
def get_all_subclasses():
return Script._get_all_subclasses(Script)
@staticmethod
def _get_all_subclasses(cls):
# hackish mess to make sure we have all the modules loaded for the scripts
pkg_dir = os.path.dirname(__file__)
for (module_loader, name, ispkg) in pkgutil.iter_modules([pkg_dir]):
importlib.import_module('.' + name, __package__)
"""Returns a list of all classes that extend this class."""
all_subclasses = []
for subclass in cls.__subclasses__():
all_subclasses.append(subclass)
all_subclasses.extend(Script._get_all_subclasses(subclass))
return all_subclasses

45
crc/scripts/study_info.py Normal file
View File

@ -0,0 +1,45 @@
import requests
from crc import session
from crc.api.common import ApiError
from crc.models.study import StudyModel, StudyModelSchema
from crc.scripts.script import Script
from crc.services.protocol_builder import ProtocolBuilderService
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', 'required_docs', 'details']
def get_description(self):
return """StudyInfo [TYPE], where TYPE is one of 'info', 'investigators','required_docs', or 'details'
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.
"""
def do_task(self, task, study_id, *args, **kwargs):
if len(args) != 1 or (args[0] not in StudyInfo.type_options):
raise ApiError(code="missing_argument",
message="The StudyInfo script requires a single argument which must be "
"one of %s" % ",".join(StudyInfo.type_options))
cmd = args[0]
if cmd == 'info':
study = session.query(StudyModel).filter_by(id=study_id).first()
schema = StudyModelSchema()
details = {"study": {"info": schema.dump(study)}}
task.data.update(details)
if cmd == 'investigators':
details = {"study": {"investigators": self.pb.get_investigators(study_id)}}
task.data.update(details)
if cmd == 'required_docs':
details = {"study": {"required_docs": self.pb.get_required_docs(study_id)}}
task.data.update(details)
if cmd == 'details':
details = {"study": {"details": self.pb.get_study_details(study_id)}}
task.data.update(details)

View File

@ -4,6 +4,7 @@ from typing import List, Optional
import requests
from crc import app
from crc.api.common import ApiError
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStudySchema, ProtocolBuilderInvestigator, \
ProtocolBuilderRequiredDocument, ProtocolBuilderStudyDetails, ProtocolBuilderInvestigatorSchema, \
ProtocolBuilderRequiredDocumentSchema, ProtocolBuilderStudyDetailsSchema
@ -17,36 +18,54 @@ class ProtocolBuilderService(object):
@staticmethod
def get_studies(user_id) -> Optional[List[ProtocolBuilderStudy]]:
if not isinstance(user_id, str):
raise ApiError("invalid_user_id", "This user id is invalid: " + str(user_id))
response = requests.get(ProtocolBuilderService.STUDY_URL % user_id)
if response.ok and response.text:
pb_studies = ProtocolBuilderStudySchema(many=True).loads(response.text)
return pb_studies
else:
return None
raise ApiError("protocol_builder_error",
"Received an invalid response from the protocol builder (status %s): %s" %
(response.status_code, response.text))
@staticmethod
def get_investigators(study_id) -> Optional[List[ProtocolBuilderInvestigator]]:
ProtocolBuilderService.check_args(study_id)
response = requests.get(ProtocolBuilderService.INVESTIGATOR_URL % study_id)
if response.ok and response.text:
pb_studies = ProtocolBuilderInvestigatorSchema(many=True).loads(response.text)
return pb_studies
else:
return None
raise ApiError("protocol_builder_error",
"Received an invalid response from the protocol builder (status %s): %s" %
(response.status_code, response.text))
@staticmethod
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:
pb_studies = ProtocolBuilderRequiredDocumentSchema(many=True).loads(response.text)
return pb_studies
else:
return None
raise ApiError("protocol_builder_error",
"Received an invalid response from the protocol builder (status %s): %s" %
(response.status_code, response.text))
@staticmethod
def get_study_details(study_id) -> Optional[ProtocolBuilderStudyDetails]:
ProtocolBuilderService.check_args(study_id)
response = requests.get(ProtocolBuilderService.STUDY_DETAILS_URL % study_id)
if response.ok and response.text:
pb_study_details = ProtocolBuilderStudyDetailsSchema().loads(response.text)
return pb_study_details
else:
return None
raise ApiError("protocol_builder_error",
"Received an invalid response from the protocol builder (status %s): %s" %
(response.status_code, response.text))
@staticmethod
def check_args(study_id):
if not isinstance(study_id, int):
raise ApiError("invalid_study_id", "This study id is invalid: " + str(study_id))

View File

@ -1,3 +1,4 @@
import re
import xml.etree.ElementTree as ElementTree
from SpiffWorkflow import Task as SpiffTask, Workflow
@ -13,6 +14,7 @@ from crc import session
from crc.api.common import ApiError
from crc.models.file import FileDataModel, FileModel, FileType
from crc.models.workflow import WorkflowStatus, WorkflowModel
from crc.scripts.script import Script
class CustomBpmnScriptEngine(BpmnScriptEngine):
@ -20,7 +22,7 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
Rather than execute arbitrary code, this assumes the script references a fully qualified python class
such as myapp.RandomFact. """
def execute(self, task, script, **kwargs):
def execute(self, task:SpiffTask, script, **kwargs):
"""
Assume that the script read in from the BPMN file is a fully qualified python class. Instantiate
that class, pass in any data available to the current task so that it might act on it.
@ -29,11 +31,31 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
This allows us to reference custom code from the BPMN diagram.
"""
commands = script.split(" ")
module_name = "crc." + commands[0]
class_name = module_name.split(".")[-1]
path_and_command = commands[0].rsplit(".", 1)
if len(path_and_command) == 1:
module_name = "crc.scripts." + self.camel_to_snake(path_and_command[0])
class_name = path_and_command[0]
else:
module_name = "crc.scripts." + path_and_command[0] + "." + self.camel_to_snake(path_and_command[1])
class_name = path_and_command[1]
try:
mod = __import__(module_name, fromlist=[class_name])
klass = getattr(mod, class_name)
klass().do_task(task, *commands[1:])
study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
if not isinstance(klass(), Script):
raise ApiError("invalid_script",
"This is an internal error. The script '%s:%s' you called "
"does not properly implement the CRC Script class." %
(module_name, class_name))
klass().do_task(task, study_id, *commands[1:])
except ModuleNotFoundError as mnfe:
raise ApiError("invalid_script",
"Unable to locate Script: '%s:%s'" % (module_name, class_name), 400)
@staticmethod
def camel_to_snake(camel):
camel = camel.strip()
return re.sub(r'(?<!^)(?=[A-Z])', '_', camel).lower()
def evaluate(self, task, expression):
"""
@ -66,7 +88,7 @@ class WorkflowProcessor(object):
_script_engine = CustomBpmnScriptEngine()
_serializer = BpmnSerializer()
WORKFLOW_ID_KEY = "workflow_id"
STUDY_ID_KEY = "session_id"
STUDY_ID_KEY = "study_id"
def __init__(self, workflow_spec_id, bpmn_json):
wf_spec = self.get_spec(workflow_spec_id)
@ -104,6 +126,7 @@ class WorkflowProcessor(object):
def create(cls, study_id, workflow_spec_id):
spec = WorkflowProcessor.get_spec(workflow_spec_id)
bpmn_workflow = BpmnWorkflow(spec, script_engine=cls._script_engine)
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study_id
bpmn_workflow.do_engine_steps()
json = cls._serializer.serialize_workflow(bpmn_workflow)
processor = cls(workflow_spec_id, json)
@ -116,7 +139,6 @@ class WorkflowProcessor(object):
# a second time to store the serilaization so we can maintain this link within
# the spiff-workflow process.
processor.bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = workflow_model.id
processor.bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study_id
workflow_model.bpmn_workflow_json = processor.serialize()
session.add(workflow_model)
session.commit()

Binary file not shown.

View File

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96a17d9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_93a29b3" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0637d8i</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0637d8i" sourceRef="StartEvent_1" targetRef="task_gather_information" />
<bpmn:userTask id="task_gather_information" name="Gather Information" camunda:formKey="example_document_form">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="full_name" label="What is your name?" type="string" />
<camunda:formField id="date" label="date" type="string" />
<camunda:formField id="title" label="Title" type="string" />
<camunda:formField id="company" label="Company" type="string" />
<camunda:formField id="last_name" label="Last Name" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0637d8i</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1i7hk1a</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1i7hk1a" sourceRef="task_gather_information" targetRef="task_generate_document" />
<bpmn:scriptTask id="task_generate_document" name="Generate Document">
<bpmn:extensionElements>
<camunda:properties>
<camunda:property name="template" />
</camunda:properties>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1i7hk1a</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_11c35oq</bpmn:outgoing>
<bpmn:script>scripts.CompleteTemplate Letter.docx</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0evb22x">
<bpmn:incoming>SequenceFlow_11c35oq</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_11c35oq" sourceRef="task_generate_document" targetRef="EndEvent_0evb22x" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_93a29b3">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0637d8i_di" bpmnElement="SequenceFlow_0637d8i">
<di:waypoint x="215" y="117" />
<di:waypoint x="265" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_02o51o8_di" bpmnElement="task_gather_information">
<dc:Bounds x="265" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1i7hk1a_di" bpmnElement="SequenceFlow_1i7hk1a">
<di:waypoint x="365" y="117" />
<di:waypoint x="465" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ScriptTask_0xjh8x4_di" bpmnElement="task_generate_document">
<dc:Bounds x="465" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0evb22x_di" bpmnElement="EndEvent_0evb22x">
<dc:Bounds x="665" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_11c35oq_di" bpmnElement="SequenceFlow_11c35oq">
<di:waypoint x="565" y="117" />
<di:waypoint x="665" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -74,37 +74,6 @@ class ExampleDataLoader:
# name="crc_study_workflow",
# display_name="CR Connect Study Workflow",
# description='Draft workflow for CR Connect studies.')
workflow_specifications += \
self.create_spec(id="random_fact",
name="random_fact",
display_name="Random Fact Generator",
description='Displays a random fact about a topic of your choosing.')
workflow_specifications += \
self.create_spec(id="two_forms",
name="two_forms",
display_name="Two dump questions on two separate tasks",
description='the name says it all')
workflow_specifications += \
self.create_spec(id="decision_table",
name="decision_table",
display_name="Form with Decision Table",
description='the name says it all')
workflow_specifications += \
self.create_spec(id="parallel_tasks",
name="parallel_tasks",
display_name="Parallel tasks",
description='Four tasks that can happen simultaneously')
workflow_specifications += \
self.create_spec(id="exclusive_gateway",
name="exclusive_gateway",
display_name="Exclusive Gateway Example",
description='How to take different paths based on input.')
workflow_specifications += \
self.create_spec(id="docx",
name="docx",
display_name="Form with document generation",
description='the name says it all')
all_data = users + studies + workflow_specifications
return all_data

View File

@ -27,7 +27,7 @@
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1i7hk1a</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_11c35oq</bpmn:outgoing>
<bpmn:script>scripts.CompleteTemplate Letter.docx</bpmn:script>
<bpmn:script>CompleteTemplate Letter.docx</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0evb22x">
<bpmn:incoming>SequenceFlow_11c35oq</bpmn:incoming>

View File

@ -130,7 +130,7 @@ Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0641sh6</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0t29gjo</bpmn:outgoing>
<bpmn:script>scripts.FactService</bpmn:script>
<bpmn:script>FactService</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="EndEvent_0u1cgrf">
<bpmn:documentation># Great Job!

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<bpmn:process id="Process_0exnnpv" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1nfe5m9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1nfe5m9" sourceRef="StartEvent_1" targetRef="Task_Script_Load_Study_Details" />
<bpmn:scriptTask id="Task_Script_Load_Study_Details" name="Load Study Info">
<bpmn:incoming>SequenceFlow_1nfe5m9</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1bqiin0</bpmn:outgoing>
<bpmn:script>StudyInfo info</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1bqiin0" sourceRef="Task_Script_Load_Study_Details" targetRef="EndEvent_171dj09" />
<bpmn:endEvent id="EndEvent_171dj09">
<bpmn:incoming>SequenceFlow_1bqiin0</bpmn:incoming>
</bpmn:endEvent>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0exnnpv">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1nfe5m9_di" bpmnElement="SequenceFlow_1nfe5m9">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ScriptTask_1mp6xid_di" bpmnElement="Task_Script_Load_Study_Details">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1bqiin0_di" bpmnElement="SequenceFlow_1bqiin0">
<di:waypoint x="370" y="117" />
<di:waypoint x="402" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_171dj09_di" bpmnElement="EndEvent_171dj09">
<dc:Bounds x="402" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -7,7 +7,6 @@ from crc.models.study import StudyModel, StudyModelSchema
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \
WorkflowApiSchema
from services.protocol_builder import ProtocolBuilderService
from tests.base_test import BaseTest

View File

@ -1,10 +1,12 @@
import json
from unittest.mock import patch
from crc import session
from crc.models.file import FileModelSchema
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, \
WorkflowApiSchema, WorkflowStatus, Task
from crc.services.workflow_processor import WorkflowProcessor
from tests.base_test import BaseTest
@ -12,7 +14,8 @@ class TestTasksApi(BaseTest):
def create_workflow(self, workflow_name):
study = session.query(StudyModel).first()
spec = session.query(WorkflowSpecModel).filter_by(id=workflow_name).first()
spec = self.load_test_spec(workflow_name)
processor = WorkflowProcessor.create(study.id, spec.id)
rv = self.app.post(
'/v1.0/study/%i/workflows' % study.id,
headers=self.logged_in_headers(),
@ -186,8 +189,6 @@ class TestTasksApi(BaseTest):
task.process_documentation(docs)
self.assertEqual("This test works", task.documentation)
def test_get_documentation_populated_in_end(self):
self.load_example_data()
workflow = self.create_workflow('random_fact')
@ -201,3 +202,7 @@ class TestTasksApi(BaseTest):
self.assertEqual("EndEvent_0u1cgrf", workflow_api.next_task['name'])
self.assertIsNotNone(workflow_api.next_task['documentation'])
self.assertTrue("norris" in workflow_api.next_task['documentation'])
# response = ProtocolBuilderService.get_study_details(self.test_study_id)

View File

@ -22,7 +22,8 @@ class TestStudyApi(BaseTest):
def test_render_docx(self):
filepath = os.path.join(app.root_path, '..', 'tests', 'data', 'table.docx')
template_data = {"hippa": [{"option": "Name", "selected": True, "stored": ["Record at UVA", "Stored Long Term"]},
template_data = {"hippa": [
{"option": "Name", "selected": True, "stored": ["Record at UVA", "Stored Long Term"]},
{"option": "Address", "selected": False},
{"option": "Phone", "selected": True, "stored": ["Send or Transmit outside of UVA"]}]}
with open(filepath, 'rb') as f:
@ -31,3 +32,11 @@ class TestStudyApi(BaseTest):
data=file_data, follow_redirects=True,
content_type='multipart/form-data')
self.assert_success(rv)
def test_list_scripts(self):
rv = self.app.get('/v1.0/list_scripts')
self.assert_success(rv)
scripts = json.loads(rv.get_data(as_text=True))
self.assertTrue(len(scripts) > 1)
self.assertIsNotNone(scripts[0]['name'])
self.assertIsNotNone(scripts[0]['description'])

View File

@ -1,5 +1,6 @@
import string
import random
from unittest.mock import patch
from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
@ -29,7 +30,7 @@ class TestWorkflowProcessor(BaseTest):
def test_create_and_complete_workflow(self):
self.load_example_data()
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="random_fact").first()
workflow_spec_model = self.load_test_spec("random_fact")
study = session.query(StudyModel).first()
processor = WorkflowProcessor.create(study.id, workflow_spec_model.id)
self.assertEqual(study.id, processor.bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY])
@ -54,9 +55,9 @@ class TestWorkflowProcessor(BaseTest):
def test_workflow_with_dmn(self):
self.load_example_data()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("decision_table")
files = session.query(FileModel).filter_by(workflow_spec_id='decision_table').all()
self.assertEqual(2, len(files))
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="decision_table").first()
processor = WorkflowProcessor.create(study.id, workflow_spec_model.id)
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
next_user_tasks = processor.next_user_tasks()
@ -79,7 +80,7 @@ class TestWorkflowProcessor(BaseTest):
def test_workflow_with_parallel_forms(self):
self.load_example_data()
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="parallel_tasks").first()
workflow_spec_model = self.load_test_spec("parallel_tasks")
study = session.query(StudyModel).first()
processor = WorkflowProcessor.create(study.id, workflow_spec_model.id)
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
@ -121,7 +122,7 @@ class TestWorkflowProcessor(BaseTest):
def test_workflow_processor_knows_the_text_task_even_when_parallel(self):
self.load_example_data()
study = session.query(StudyModel).first()
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="parallel_tasks").first()
workflow_spec_model = self.load_test_spec("parallel_tasks")
processor = WorkflowProcessor.create(study.id, workflow_spec_model.id)
self.assertEqual(WorkflowStatus.user_input_required, processor.get_status())
next_user_tasks = processor.next_user_tasks()
@ -141,7 +142,7 @@ class TestWorkflowProcessor(BaseTest):
def test_workflow_processor_returns_next_task_as_end_task_if_complete(self):
self.load_example_data()
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="random_fact").first()
workflow_spec_model = self.load_test_spec("random_fact")
study = session.query(StudyModel).first()
processor = WorkflowProcessor.create(study.id, workflow_spec_model.id)
processor.do_engine_steps()
@ -196,3 +197,20 @@ class TestWorkflowProcessor(BaseTest):
self.assertIsNotNone(file_data.data)
self.assertTrue(len(file_data.data) > 0)
# Not going any farther here, assuming this is tested in libraries correctly.
def test_load_study_information(self):
""" Test a workflow that includes requests to pull in Study Details."""
self.load_example_data()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("study_details")
processor = WorkflowProcessor.create(study.id, workflow_spec_model.id)
processor.do_engine_steps()
task = processor.bpmn_workflow.last_task
self.assertIsNotNone(task.data)
self.assertIn("study", task.data)
self.assertIn("info", task.data["study"])
self.assertIn("title", task.data["study"]["info"])
self.assertIn("last_updated", task.data["study"]["info"])
self.assertIn("sponsor", task.data["study"]["info"])

View File

@ -47,7 +47,7 @@ class TestWorkflowSpec(BaseTest):
def test_delete_workflow_specification(self):
self.load_example_data()
spec_id = 'random_fact'
self.load_test_spec(spec_id)
num_specs_before = session.query(WorkflowSpecModel).filter_by(id=spec_id).count()
self.assertEqual(num_specs_before, 1)