This commit is contained in:
mike cullerton 2021-07-07 14:33:17 -04:00
commit 05ba28b67b
16 changed files with 247 additions and 13 deletions

2
Pipfile.lock generated
View File

@ -979,7 +979,7 @@
},
"spiffworkflow": {
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "66555b92ef1d8d9ce117b6f2ccf6aa248df9835f"
"ref": "59d12e25e5313977b83e7d65b6deb65572dee71c"
},
"sqlalchemy": {
"hashes": [

View File

@ -63,6 +63,7 @@ PB_REQUIRED_DOCS_URL = environ.get('PB_REQUIRED_DOCS_URL', default=PB_BASE_URL +
PB_STUDY_DETAILS_URL = environ.get('PB_STUDY_DETAILS_URL', default=PB_BASE_URL + "study?studyid=%i")
PB_SPONSORS_URL = environ.get('PB_SPONSORS_URL', default=PB_BASE_URL + "sponsors?studyid=%i")
PB_IRB_INFO_URL = environ.get('PB_IRB_INFO_URL', default=PB_BASE_URL + "current_irb_info/%i")
PB_CHECK_STUDY_URL = environ.get('PB_CHECK_STUDY_URL', default=PB_BASE_URL + "check_study/%i")
# Ldap Configuration
LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu").strip('/') # No trailing slash or http://

View File

@ -68,6 +68,7 @@ class FileDataModel(db.Model):
date_created = db.Column(db.DateTime(timezone=True), server_default=func.now())
file_model_id = db.Column(db.Integer, db.ForeignKey('file.id'))
file_model = db.relationship("FileModel", foreign_keys=[file_model_id])
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=True)
class FileModel(db.Model):
@ -114,6 +115,7 @@ class File(object):
instance.last_modified = data_model.date_created
instance.latest_version = data_model.version
instance.size = data_model.size
instance.user_uid = data_model.user_uid
else:
instance.last_modified = None
instance.latest_version = None
@ -141,7 +143,7 @@ class FileSchema(Schema):
fields = ["id", "name", "is_status", "is_reference", "content_type",
"primary", "primary_process_id", "workflow_spec_id", "workflow_id",
"irb_doc_code", "last_modified", "latest_version", "type", "size", "data_store",
"document"]
"document", "user_uid"]
unknown = INCLUDE
type = EnumField(FileType)

View File

@ -0,0 +1,30 @@
from crc.scripts.script import Script
from crc.api.common import ApiError
from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.study_service import StudyService
class CheckStudy(Script):
pb = ProtocolBuilderService()
def get_description(self):
return """Returns the Check Study data for a Study"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
study = StudyService.get_study(study_id)
if study:
return {"DETAIL": "Passed validation.", "STATUS": "No Error"}
else:
raise ApiError.from_task(code='bad_study',
message=f'No study for study_id {study_id}',
task=task)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
check_study = self.pb.check_study(study_id)
if check_study:
return check_study
else:
raise ApiError.from_task(code='missing_check_study',
message='There was a problem checking information for this study.',
task=task)

View File

@ -19,6 +19,7 @@ from crc.models.data_store import DataStoreModel
from crc.models.file import FileType, FileDataModel, FileModel, LookupFileModel, LookupDataModel
from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecDependencyFile
from crc.services.cache_service import cache
from crc.services.user_service import UserService
import re
@ -168,10 +169,14 @@ class FileService(object):
except XMLSyntaxError as xse:
raise ApiError("invalid_xml", "Failed to parse xml: " + str(xse), file_name=file_model.name)
try:
user_uid = UserService.current_user().uid
except ApiError as ae:
user_uid = None
new_file_data_model = FileDataModel(
data=binary_data, file_model_id=file_model.id, file_model=file_model,
version=version, md5_hash=md5_checksum, date_created=datetime.utcnow(),
size=size
size=size, user_uid=user_uid
)
session.add_all([file_model, new_file_data_model])
session.commit()

View File

@ -15,6 +15,7 @@ class ProtocolBuilderService(object):
STUDY_DETAILS_URL = app.config['PB_STUDY_DETAILS_URL']
SPONSORS_URL = app.config['PB_SPONSORS_URL']
IRB_INFO_URL = app.config['PB_IRB_INFO_URL']
CHECK_STUDY_URL = app.config['PB_CHECK_STUDY_URL']
@staticmethod
def is_enabled():
@ -64,6 +65,10 @@ class ProtocolBuilderService(object):
def get_sponsors(study_id) -> {}:
return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.SPONSORS_URL)
@staticmethod
def check_study(study_id) -> {}:
return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.CHECK_STUDY_URL)
@staticmethod
def __enabled_or_raise():
if not ProtocolBuilderService.is_enabled():

View File

@ -7,7 +7,7 @@ import shlex
from datetime import datetime
from typing import List
from SpiffWorkflow import Task as SpiffTask, WorkflowException
from SpiffWorkflow import Task as SpiffTask, WorkflowException, Task
from SpiffWorkflow.bpmn.BpmnScriptEngine import BpmnScriptEngine
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from SpiffWorkflow.bpmn.serializer.BpmnSerializer import BpmnSerializer
@ -30,6 +30,8 @@ from crc.services.file_service import FileService
from crc import app
from crc.services.user_service import UserService
from difflib import SequenceMatcher
class CustomBpmnScriptEngine(BpmnScriptEngine):
"""This is a custom script processor that can be easily injected into Spiff Workflow.
It will execute python code read in from the bpmn. It will also make any scripts in the
@ -50,13 +52,11 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
else:
workflow_id = None
try:
if task.workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY]:
augment_methods = Script.generate_augmented_validate_list(task, study_id, workflow_id)
else:
augment_methods = Script.generate_augmented_list(task, study_id, workflow_id)
super().execute(task, script, data, external_methods=augment_methods)
except WorkflowException as e:
raise e
@ -337,6 +337,9 @@ class WorkflowProcessor(object):
if bpmn_workflow.is_completed():
return WorkflowStatus.complete
user_tasks = bpmn_workflow.get_ready_user_tasks()
waiting_tasks = bpmn_workflow.get_tasks(Task.WAITING)
if len(waiting_tasks) > 0:
return WorkflowStatus.waiting
if len(user_tasks) > 0:
return WorkflowStatus.user_input_required
else:

View File

@ -0,0 +1,27 @@
"""add user_uid column to file_data table
Revision ID: 30e017a03948
Revises: bbf064082623
Create Date: 2021-07-06 10:39:04.661704
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '30e017a03948'
down_revision = 'bbf064082623'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('file_data', sa.Column('user_uid', sa.String(), nullable=True))
op.create_foreign_key(None, 'file_data', 'user', ['user_uid'], ['uid'])
def downgrade():
# op.drop_constraint('file_data_user_uid_fkey', 'file_data', type_='foreignkey')
# op.execute("update file_data set user_uid = NULL WHERE user_uid IS NOT NULL")
op.drop_column('file_data', 'user_uid')

View File

@ -148,13 +148,6 @@ class BaseTest(unittest.TestCase):
otherwise it depends on a small setup for running tests."""
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
if use_crc_data:
ExampleDataLoader().load_all()
elif use_rrt_data:
ExampleDataLoader().load_rrt()
else:
ExampleDataLoader().load_test_data()
# If in production mode, only add the first user.
if app.config['PRODUCTION']:
session.add(UserModel(**self.users[0]))
@ -162,6 +155,13 @@ class BaseTest(unittest.TestCase):
for user_json in self.users:
session.add(UserModel(**user_json))
if use_crc_data:
ExampleDataLoader().load_all()
elif use_rrt_data:
ExampleDataLoader().load_rrt()
else:
ExampleDataLoader().load_test_data()
session.commit()
for study_json in self.studies:
study_model = StudyModel(**study_json)

View File

@ -0,0 +1,53 @@
<?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_3fd9241" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_9d7b2c2" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_17nzcku</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_17nzcku" sourceRef="StartEvent_1" targetRef="Activity_GetCheckStudy" />
<bpmn:scriptTask id="Activity_GetCheckStudy" name="Get Check Study">
<bpmn:incoming>Flow_17nzcku</bpmn:incoming>
<bpmn:outgoing>Flow_0oozrfg</bpmn:outgoing>
<bpmn:script>check_study = check_study()</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0oozrfg" sourceRef="Activity_GetCheckStudy" targetRef="Activity_DisplayCheckStudy" />
<bpmn:manualTask id="Activity_DisplayCheckStudy" name="Display Check Study">
<bpmn:documentation># Check Study
&lt;div&gt;&lt;span&gt;{{check_study}}&lt;/span&gt;&lt;/div&gt;</bpmn:documentation>
<bpmn:incoming>Flow_0oozrfg</bpmn:incoming>
<bpmn:outgoing>Flow_10sc31i</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_0embsc7">
<bpmn:incoming>Flow_10sc31i</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_10sc31i" sourceRef="Activity_DisplayCheckStudy" targetRef="Event_0embsc7" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_9d7b2c2">
<bpmndi:BPMNEdge id="Flow_10sc31i_di" bpmnElement="Flow_10sc31i">
<di:waypoint x="530" y="177" />
<di:waypoint x="592" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0oozrfg_di" bpmnElement="Flow_0oozrfg">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_17nzcku_di" bpmnElement="Flow_17nzcku">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1f9d5ew_di" bpmnElement="Activity_GetCheckStudy">
<dc:Bounds x="270" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_01vscea_di" bpmnElement="Activity_DisplayCheckStudy">
<dc:Bounds x="430" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0embsc7_di" bpmnElement="Event_0embsc7">
<dc:Bounds x="592" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,3 @@
[
{"DETAIL": "Passed validation.", "STATUS": "No Error"}
]

View File

@ -0,0 +1,57 @@
<?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" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_8983dae" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_2a4c7a5" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_13jyds8</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_13jyds8" sourceRef="StartEvent_1" targetRef="Activity_GetData" />
<bpmn:endEvent id="Event_03x966p">
<bpmn:incoming>Flow_18kybym</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_18kybym" sourceRef="Activity_RunScript" targetRef="Event_03x966p" />
<bpmn:scriptTask id="Activity_RunScript" name="Run Script">
<bpmn:incoming>Flow_1jqzan6</bpmn:incoming>
<bpmn:outgoing>Flow_18kybym</bpmn:outgoing>
<bpmn:script>print(ham)</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1jqzan6" sourceRef="Activity_GetData" targetRef="Activity_RunScript" />
<bpmn:userTask id="Activity_GetData" name="Get Data" camunda:formKey="DataForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="user" label="User" type="string" defaultValue="World" />
<camunda:formField id="spam" label="Spam" type="boolean" defaultValue="False" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_13jyds8</bpmn:incoming>
<bpmn:outgoing>Flow_1jqzan6</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_2a4c7a5">
<bpmndi:BPMNEdge id="Flow_18kybym_di" bpmnElement="Flow_18kybym">
<di:waypoint x="370" y="177" />
<di:waypoint x="432" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13jyds8_di" bpmnElement="Flow_13jyds8">
<di:waypoint x="48" y="177" />
<di:waypoint x="90" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1jqzan6_di" bpmnElement="Flow_1jqzan6">
<di:waypoint x="190" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Event_03x966p_di" bpmnElement="Event_03x966p">
<dc:Bounds x="432" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1d9d2u8_di" bpmnElement="Activity_RunScript">
<dc:Bounds x="270" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="12" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_10ypwag_di" bpmnElement="Activity_GetData">
<dc:Bounds x="90" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -126,6 +126,7 @@ class TestFilesApi(BaseTest):
self.assertEqual(FileType.xlsx, file.type)
self.assertTrue(file.is_reference)
self.assertEqual("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.content_type)
self.assertEqual('dhf8r', json_data['user_uid'])
def test_set_reference_file_bad_extension(self):
file_name = DocumentService.DOCUMENT_LIST

View File

@ -0,0 +1,25 @@
from tests.base_test import BaseTest
from crc import app
from unittest.mock import patch
class TestCheckStudy(BaseTest):
def test_check_study_script_validation(self):
self.load_example_data()
spec_model = self.load_test_spec('check_study_script')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
self.assertEqual([], rv.json)
@patch('crc.services.protocol_builder.requests.get')
def test_check_study(self, mock_get):
app.config['PB_ENABLED'] = True
app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('check_study.json')
workflow = self.create_workflow('check_study_script')
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
self.assertIn('DETAIL', task.documentation)
self.assertIn('STATUS', task.documentation)

View File

@ -72,3 +72,13 @@ class TestProtocolBuilder(BaseTest):
self.assertEqual('IRB Event 1', response[0]["IRBEVENT"])
self.assertEqual('IRB Event 2', response[1]["IRBEVENT"])
self.assertEqual('IRB Event 3', response[2]["IRBEVENT"])
@patch('crc.services.protocol_builder.requests.get')
def test_check_study(self, mock_get):
app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('check_study.json')
response = ProtocolBuilderService.check_study(self.test_study_id)
self.assertIsNotNone(response)
self.assertIn('DETAIL', response[0].keys())
self.assertIn('STATUS', response[0].keys())

View File

@ -0,0 +1,12 @@
from tests.base_test import BaseTest
import json
class TestNameErrorHint(BaseTest):
def test_name_error_hint(self):
self.load_example_data()
spec_model = self.load_test_spec('script_with_name_error')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
json_data = json.loads(rv.get_data(as_text=True))
self.assertIn('Did you mean \'[\'spam\'', json_data[0]['message'])