Merge branch 'dev' into study-associated-email-207-223

This commit is contained in:
Mike Cullerton 2021-03-11 12:38:47 -05:00 committed by GitHub
commit e7c78c8d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 299 additions and 73 deletions

View File

@ -63,7 +63,7 @@ if app.config['SENTRY_ENVIRONMENT']:
# Connexion Error handling
def render_errors(exception):
from crc.api.common import ApiError, ApiErrorSchema
error = ApiError(code=exception.title, message=exception.details, status_code=exception.status)
error = ApiError(code=exception.title, message=exception.detail, status_code=exception.status)
return Response(ApiErrorSchema().dump(error), status=401, mimetype="application/json")

View File

@ -9,10 +9,8 @@ from jinja2 import Template, UndefinedError
from crc.api.common import ApiError
from crc.scripts.complete_template import CompleteTemplate
from crc.scripts.script import Script
import crc.scripts
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.email_service import EmailService
from crc.services.workflow_processor import WorkflowProcessor, CustomBpmnScriptEngine
from config.default import DEFAULT_SENDER
@ -81,13 +79,13 @@ def evaluate_python_expression(body):
front end application needs to do real-time processing on task data. If for instance
there is a hide expression that is based on a previous value in the same form."""
try:
# fixme: The script engine should be pulled from Workflow Processor,
# but the one it returns overwrites the evaluate expression making it uncallable.
script_engine = PythonScriptEngine()
result = script_engine.evaluate(body['expression'], **body['data'])
script_engine = CustomBpmnScriptEngine()
result = script_engine.eval(body['expression'], body['data'])
return {"result": result}
except Exception as e:
raise ApiError("expression_error", str(e))
raise ApiError("expression_error", f"Failed to evaluate the expression '%s'. %s" %
(body['expression'], str(e)),
task_data = body["data"])
def send_test_email(subject, address, message, data=None):
@ -98,4 +96,3 @@ def send_test_email(subject, address, message, data=None):
recipients=[address],
content=rendered,
content_html=wrapped)

View File

@ -120,7 +120,7 @@ def restart_workflow(workflow_id, clear_data=False):
"""Restart a workflow with the latest spec.
Clear data allows user to restart the workflow without previous data."""
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
WorkflowProcessor(workflow_model).reset(workflow_model, clear_data=clear_data)
WorkflowProcessor.reset(workflow_model, clear_data=clear_data)
return get_workflow(workflow_model.id)

View File

@ -9,6 +9,7 @@ from ldap3.core.exceptions import LDAPSocketOpenError
from crc import db, session, app
from crc.api.common import ApiError
from crc.models.email import EmailModel
from crc.models.file import FileDataModel, FileModel, FileModelSchema, File, LookupFileModel, LookupDataModel
from crc.models.ldap import LdapSchema
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
@ -210,6 +211,10 @@ class StudyService(object):
@staticmethod
def delete_study(study_id):
session.query(TaskEventModel).filter_by(study_id=study_id).delete()
session.query(StudyAssociated).filter_by(study_id=study_id).delete()
session.query(EmailModel).filter_by(study_id=study_id).delete()
session.query(StudyEvent).filter_by(study_id=study_id).delete()
for workflow in session.query(WorkflowModel).filter_by(study_id=study_id):
StudyService.delete_workflow(workflow.id)
study = session.query(StudyModel).filter_by(id=study_id).first()

View File

@ -62,7 +62,7 @@ class UserService(object):
if uid is None:
raise ApiError("invalid_uid", "Please provide a valid user uid.")
if not UserService.admin_is_impersonating() and UserService.is_different_user(uid):
if UserService.is_different_user(uid):
# Impersonate the user if the given uid is valid.
impersonate_user = session.query(UserModel).filter(UserModel.uid == uid).first()
@ -70,6 +70,7 @@ class UserService(object):
g.impersonate_user = impersonate_user
# Store the uid and user session token.
session.query(AdminSessionModel).filter(AdminSessionModel.token==g.token).delete()
session.add(AdminSessionModel(token=g.token, admin_impersonate_uid=uid))
session.commit()
else:

View File

@ -68,43 +68,6 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
f'something you are referencing does not exist:'
f' {script}, {e}')
# else:
# self.run_predefined_script(task, script[2:], data) # strip off the first two characters.
# def run_predefined_script(self, task: SpiffTask, script, data):
# commands = shlex.split(script)
# 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)
# study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
# if WorkflowProcessor.WORKFLOW_ID_KEY in task.workflow.data:
# workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
# else:
# workflow_id = None
#
# if not isinstance(klass(), Script):
# raise ApiError.from_task("invalid_script",
# "This is an internal error. The script '%s:%s' you called " %
# (module_name, class_name) +
# "does not properly implement the CRC Script class.",
# task=task)
# if task.workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY]:
# """If this is running a validation, and not a normal process, then we want to
# mimic running the script, but not make any external calls or database changes."""
# klass().do_task_validate_only(task, study_id, workflow_id, *commands[1:])
# else:
# klass().do_task(task, study_id, workflow_id, *commands[1:])
# except ModuleNotFoundError:
# raise ApiError.from_task("invalid_script",
# "Unable to locate Script: '%s:%s'" % (module_name, class_name),
# task=task)
def evaluate_expression(self, task, expression):
"""
@ -130,6 +93,10 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
"Error evaluating expression "
"'%s', %s" % (expression, str(e)))
def eval(self, exp, data):
return super()._eval(exp, {}, **data)
@staticmethod
def camel_to_snake(camel):
@ -206,10 +173,19 @@ class WorkflowProcessor(object):
else:
self.is_latest_spec = False
def reset(self, workflow_model, clear_data=False):
@staticmethod
def reset(workflow_model, clear_data=False):
print('WorkflowProcessor: reset: ')
self.cancel_notify()
# Try to execute a cancel notify
try:
wp = WorkflowProcessor(workflow_model)
wp.cancel_notify() # The executes a notification to all endpoints that
except Exception as e:
app.logger.error(f"Unable to send a cancel notify for workflow %s during a reset."
f" Continuing with the reset anyway so we don't get in an unresolvable"
f" state. An %s error occured with the following information: %s" %
(workflow_model.id, e.__class__.__name__, str(e)))
workflow_model.bpmn_workflow_json = None
if clear_data:
# Clear form_data from task_events
@ -219,7 +195,7 @@ class WorkflowProcessor(object):
task_event.form_data = {}
session.add(task_event)
session.commit()
return self.__init__(workflow_model)
return WorkflowProcessor(workflow_model)
def __get_bpmn_workflow(self, workflow_model: WorkflowModel, spec: WorkflowSpec, validate_only=False):
if workflow_model.bpmn_workflow_json:

View File

@ -156,9 +156,10 @@ class WorkflowService(object):
# If a field is hidden and required, it must have a default value or value_expression
if field.has_property(Task.FIELD_PROP_HIDE_EXPRESSION) and field.has_validation(Task.FIELD_CONSTRAINT_REQUIRED):
if not field.has_property(Task.FIELD_PROP_VALUE_EXPRESSION) or not (hasattr(field, 'default_value')):
if not field.has_property(Task.FIELD_PROP_VALUE_EXPRESSION) and \
(not (hasattr(field, 'default_value')) or field.default_value is None):
raise ApiError(code='hidden and required field missing default',
message='Fields that are required but can be hidden must have either a default value or a value_expression',
message=f'Field "{field.id}" is required but can be hidden. It must have either a default value or a value_expression',
task_id='task.id',
task_name=task.get_name())
@ -262,12 +263,13 @@ class WorkflowService(object):
# If no default exists, return None
# Note: if default is False, we don't want to execute this code
if default is None:
return None
if default is None or (isinstance(default, str) and default.strip() == ''):
if field.type == "enum" or field.type == "autocomplete":
return {'value': None, 'label': None}
else:
return None
if field.type == "enum" and not has_lookup:
if isinstance(default, str) and default.strip() == '':
return
default_option = next((obj for obj in field.options if obj.id == default), None)
if not default_option:
raise ApiError.from_task("invalid_default", "You specified a default value that does not exist in "
@ -729,5 +731,4 @@ class WorkflowService(object):
workflows = db.session.query(WorkflowModel).filter_by(study_id=study_id).all()
for workflow in workflows:
if workflow.status == WorkflowStatus.user_input_required or workflow.status == WorkflowStatus.waiting:
processor = WorkflowProcessor(workflow)
processor.reset(workflow)
WorkflowProcessor.reset(workflow, clear_data=False)

View File

@ -1,5 +1,5 @@
<?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_1mhc2v8" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<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_1mhc2v8" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_HiddenRequired" name="Hidden Reguired Field" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0zt7wv5</bpmn:outgoing>
@ -32,7 +32,9 @@ if not 'hide_yes_no' in globals():
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0fb4w15" sourceRef="Activity_PreData" targetRef="Activity_HiddenField" />
<bpmn:manualTask id="Activity_GoodBye" name="Good Bye">
<bpmn:documentation>&lt;H1&gt;Good Bye{% if name %} {{ name }}{% endif %}&lt;/H1&gt;</bpmn:documentation>
<bpmn:documentation>&lt;H1&gt;Good Bye{% if name %} {{ name }}{% endif %}&lt;/H1&gt;
&lt;div&gt;&lt;span&gt;Color is {{ color }}&lt;/span&gt;&lt;/div&gt;
</bpmn:documentation>
<bpmn:incoming>Flow_1qkjkbh</bpmn:incoming>
<bpmn:outgoing>Flow_1udbzd6</bpmn:outgoing>
</bpmn:manualTask>

View File

@ -0,0 +1,93 @@
<?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_1mhc2v8" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_HiddenRequired" name="Hidden Reguired Field" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0zt7wv5</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0zt7wv5" sourceRef="StartEvent_1" targetRef="Activity_Hello" />
<bpmn:userTask id="Activity_HiddenField" name="Hidden Field" camunda:formKey="HiddenFieldForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="name" label="Name" type="string" defaultValue="World">
<camunda:properties>
<camunda:property id="hide_expression" value="hide_yes_no" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="require_yes_no" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0fb4w15</bpmn:incoming>
<bpmn:outgoing>Flow_0c2rym0</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0cm6imh" sourceRef="Activity_Hello" targetRef="Activity_PreData" />
<bpmn:scriptTask id="Activity_PreData" name="Pre Data">
<bpmn:incoming>Flow_0cm6imh</bpmn:incoming>
<bpmn:outgoing>Flow_0fb4w15</bpmn:outgoing>
<bpmn:script>if not 'require_yes_no' in globals():
require_yes_no = True
if not 'hide_yes_no' in globals():
hide_yes_no = True</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0fb4w15" sourceRef="Activity_PreData" targetRef="Activity_HiddenField" />
<bpmn:manualTask id="Activity_GoodBye" name="Good Bye">
<bpmn:documentation>&lt;H1&gt;Good Bye{% if name %} {{ name }}{% endif %}&lt;/H1&gt;
</bpmn:documentation>
<bpmn:incoming>Flow_0c2rym0</bpmn:incoming>
<bpmn:outgoing>Flow_1udbzd6</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_194gjyj">
<bpmn:incoming>Flow_1udbzd6</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1udbzd6" sourceRef="Activity_GoodBye" targetRef="Event_194gjyj" />
<bpmn:manualTask id="Activity_Hello" name="Hello">
<bpmn:documentation>&lt;H1&gt;Hello&lt;/H1&gt;</bpmn:documentation>
<bpmn:incoming>Flow_0zt7wv5</bpmn:incoming>
<bpmn:outgoing>Flow_0cm6imh</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0c2rym0" sourceRef="Activity_HiddenField" targetRef="Activity_GoodBye" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_HiddenRequired">
<bpmndi:BPMNEdge id="Flow_0fb4w15_di" bpmnElement="Flow_0fb4w15">
<di:waypoint x="530" y="117" />
<di:waypoint x="590" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0cm6imh_di" bpmnElement="Flow_0cm6imh">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0zt7wv5_di" bpmnElement="Flow_0zt7wv5">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c2rym0_di" bpmnElement="Flow_0c2rym0">
<di:waypoint x="690" y="117" />
<di:waypoint x="750" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1udbzd6_di" bpmnElement="Flow_1udbzd6">
<di:waypoint x="850" y="117" />
<di:waypoint x="912" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0a4wzou_di" bpmnElement="Activity_HiddenField">
<dc:Bounds x="590" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0kjyqk8_di" bpmnElement="Activity_PreData">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0v7ietz_di" bpmnElement="Activity_Hello">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_12r6tn2_di" bpmnElement="Activity_GoodBye">
<dc:Bounds x="750" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_194gjyj_di" bpmnElement="Event_194gjyj">
<dc:Bounds x="912" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,96 @@
<?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_1mhc2v8" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_HiddenRequired" name="Hidden Reguired Field" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0zt7wv5</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0zt7wv5" sourceRef="StartEvent_1" targetRef="Activity_Hello" />
<bpmn:userTask id="Activity_HiddenField" name="Hidden Field" camunda:formKey="HiddenFieldForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="name" label="Name" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="True" />
<camunda:property id="value_expression" value="value_expression_value" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="True" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0fb4w15</bpmn:incoming>
<bpmn:outgoing>Flow_0c2rym0</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0cm6imh" sourceRef="Activity_Hello" targetRef="Activity_PreData" />
<bpmn:scriptTask id="Activity_PreData" name="Pre Data">
<bpmn:incoming>Flow_0cm6imh</bpmn:incoming>
<bpmn:outgoing>Flow_0fb4w15</bpmn:outgoing>
<bpmn:script>if not 'require_yes_no' in globals():
require_yes_no = True
if not 'hide_yes_no' in globals():
hide_yes_no = True
if not 'value_expression_value' in globals():
value_expression_value = 'World'</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0fb4w15" sourceRef="Activity_PreData" targetRef="Activity_HiddenField" />
<bpmn:manualTask id="Activity_GoodBye" name="Good Bye">
<bpmn:documentation>&lt;H1&gt;Good Bye{% if name %} {{ name }}{% endif %}&lt;/H1&gt;
</bpmn:documentation>
<bpmn:incoming>Flow_0c2rym0</bpmn:incoming>
<bpmn:outgoing>Flow_1udbzd6</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:endEvent id="Event_194gjyj">
<bpmn:incoming>Flow_1udbzd6</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1udbzd6" sourceRef="Activity_GoodBye" targetRef="Event_194gjyj" />
<bpmn:manualTask id="Activity_Hello" name="Hello">
<bpmn:documentation>&lt;H1&gt;Hello&lt;/H1&gt;</bpmn:documentation>
<bpmn:incoming>Flow_0zt7wv5</bpmn:incoming>
<bpmn:outgoing>Flow_0cm6imh</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0c2rym0" sourceRef="Activity_HiddenField" targetRef="Activity_GoodBye" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_HiddenRequired">
<bpmndi:BPMNEdge id="Flow_0fb4w15_di" bpmnElement="Flow_0fb4w15">
<di:waypoint x="530" y="117" />
<di:waypoint x="590" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0cm6imh_di" bpmnElement="Flow_0cm6imh">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0zt7wv5_di" bpmnElement="Flow_0zt7wv5">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0c2rym0_di" bpmnElement="Flow_0c2rym0">
<di:waypoint x="690" y="117" />
<di:waypoint x="750" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1udbzd6_di" bpmnElement="Flow_1udbzd6">
<di:waypoint x="850" y="117" />
<di:waypoint x="912" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0a4wzou_di" bpmnElement="Activity_HiddenField">
<dc:Bounds x="590" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0kjyqk8_di" bpmnElement="Activity_PreData">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0v7ietz_di" bpmnElement="Activity_Hello">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_12r6tn2_di" bpmnElement="Activity_GoodBye">
<dc:Bounds x="750" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_194gjyj_di" bpmnElement="Event_194gjyj">
<dc:Bounds x="912" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,17 +1,19 @@
import json
from profile import Profile
from crc.services.ldap_service import LdapService
from tests.base_test import BaseTest
from datetime import datetime, timezone
from unittest.mock import patch
from crc.models.email import EmailModel
from crc import session, app
from crc.models.protocol_builder import ProtocolBuilderStatus, \
ProtocolBuilderStudySchema
from crc.models.file import FileModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyEvent, StudyModel, StudySchema, StudyStatus, StudyEventType
from crc.models.study import StudyEvent, StudyModel, StudySchema, StudyStatus, StudyEventType, StudyAssociated
from crc.models.workflow import WorkflowSpecModel, WorkflowModel
from crc.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor
@ -274,7 +276,7 @@ class TestStudyApi(BaseTest):
self.assertTrue(file.archived)
self.assertIsNone(file.workflow_id)
def test_delete_study_with_workflow_and_status(self):
def test_delete_study_with_workflow_and_status_etc(self):
self.load_example_data()
workflow = session.query(WorkflowModel).first()
stats1 = StudyEvent(
@ -284,6 +286,14 @@ class TestStudyApi(BaseTest):
event_type=StudyEventType.user,
user_uid=self.users[0]['uid'],
)
LdapService.user_info('dhf8r') # Assure that there is a dhf8r in ldap for StudyAssociated.
email = EmailModel(subject="x", study_id=workflow.study_id)
associate = StudyAssociated(study_id=workflow.study_id, uid=self.users[0]['uid'])
event = StudyEvent(study_id=workflow.study_id)
session.add_all([email, associate, event])
stats2 = TaskEventModel(study_id=workflow.study_id, workflow_id=workflow.id, user_uid=self.users[0]['uid'])
session.add_all([stats1, stats2])
session.commit()
@ -293,7 +303,6 @@ class TestStudyApi(BaseTest):
self.assertIsNone(del_study)
# """
# Workflow Specs that have been made available (or not) to a particular study via the status.bpmn should be flagged
# as available (or not) when the list of a study's workflows is retrieved.

View File

@ -66,7 +66,7 @@ class DataStoreTest(BaseTest):
def test_update_study(self):
def test_update_datastore(self):
self.load_example_data()
new_study = self.add_test_study_data()
new_study = session.query(DataStoreModel).filter_by(id=new_study["id"]).first()
@ -87,7 +87,7 @@ class DataStoreTest(BaseTest):
def test_delete_study(self):
def test_delete_datastore(self):
self.load_example_data()
new_study = self.add_test_study_data()
oldid = new_study['id']

View File

@ -54,8 +54,7 @@ class TestLookupService(BaseTest):
# restart the workflow, so it can pick up the changes.
processor = WorkflowProcessor(workflow)
processor.reset(workflow)
processor = WorkflowProcessor.reset(workflow)
workflow = processor.workflow_model
LookupService.lookup(workflow, "Task_Enum_Lookup", "sponsor", "sam", limit=10)
@ -92,8 +91,7 @@ class TestLookupService(BaseTest):
results = LookupService.lookup(workflow, task.task_spec.name, "selectedItem", "", value="apples", limit=10)
self.assertEqual(0, len(results), "We shouldn't find our fruits mixed in with our animals.")
processor.reset(workflow, clear_data=True)
processor = WorkflowProcessor.reset(workflow, clear_data=True)
processor.do_engine_steps()
task = processor.get_ready_user_tasks()[0]
task.data = {"type": "fruit"}

View File

@ -213,6 +213,27 @@ class TestTasksApi(BaseTest):
self.assertTrue(workflow_api.spec_version.startswith("v2 "))
self.assertTrue(workflow_api.is_latest_spec)
def test_reset_workflow_from_broken_spec(self):
# Start the basic two_forms workflow and complete a task.
workflow = self.create_workflow('two_forms')
workflow_api = self.get_workflow_api(workflow)
self.complete_form(workflow, workflow_api.next_task, {"color": "blue"})
self.assertTrue(workflow_api.is_latest_spec)
# Break the bpmn json
workflow.bpmn_workflow_json = '{"something":"broken"}'
session.add(workflow)
session.commit()
# Try to load the workflow, we should get an error
with self.assertRaises(Exception):
workflow_api = self.complete_form(workflow, workflow_api.next_task, {"name": "Dan"})
# Now, Reset the workflow, and we should not get an error
workflow_api = self.restart_workflow_api(workflow_api, clear_data=True)
self.assertIsNotNone(workflow_api)
def test_manual_task_with_external_documentation(self):
workflow = self.create_workflow('manual_task_with_external_documentation')

View File

@ -48,6 +48,7 @@ class TestStudyApi(BaseTest):
response = json.loads(rv.get_data(as_text=True))
self.assertEqual(True, response['result'])
def test_eval_expression_with_strings(self):
"""Assures we can use python to process a value expression from the front end"""
rv = self.app.put('/v1.0/eval',
@ -81,3 +82,14 @@ CR Connect
self.assertEqual(subject, outbox[0].subject)
self.assertEqual(['user@example.com'], outbox[0].recipients)
self.assertIn('Person of Interest', outbox[0].body)
def test_eval_to_boolean_expression_with_dot_notation(self):
"""Assures we can use python to process a value expression from the front end"""
rv = self.app.put('/v1.0/eval',
data='{"expression": "test.value", "data": {"test":{"value": true}}}',
follow_redirects=True,
content_type='application/json',
headers=self.logged_in_headers())
self.assert_success(rv)
response = json.loads(rv.get_data(as_text=True))
self.assertEqual(True, response['result'])

View File

@ -14,6 +14,17 @@ class TestWorkflowHiddenRequiredField(BaseTest):
self.assertEqual(json_data[0]['code'], 'hidden and required field missing default')
self.assertIn('task_id', json_data[0])
self.assertIn('task_name', json_data[0])
self.assertIn('Field "name" is required but can be hidden', json_data[0]['message'])
def test_require_default_pass(self):
spec_model = self.load_test_spec('hidden_required_field_pass')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
self.assertEqual(0, len(rv.json))
def test_require_default_pass_expression(self):
spec_model = self.load_test_spec('hidden_required_field_pass_expression')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
self.assertEqual(0, len(rv.json))
def test_default_used(self):
# If a field is hidden and required, make sure we use the default value

View File

@ -279,7 +279,7 @@ class TestWorkflowProcessor(BaseTest):
self.assertFalse(processor2.is_latest_spec) # Still at version 1.
# Do a hard reset, which should bring us back to the beginning, but retain the data.
processor2.reset(processor2.workflow_model)
processor2 = WorkflowProcessor.reset(processor2.workflow_model)
processor3 = WorkflowProcessor(processor.workflow_model)
processor3.do_engine_steps()
self.assertEqual("Step 1", processor3.next_task().task_spec.description)

View File

@ -14,7 +14,11 @@ class TestValueExpression(BaseTest):
workflow_api = self.get_workflow_api(workflow)
second_task = workflow_api.next_task
self.assertEqual('', second_task.data['value_expression_value'])
self.assertNotIn('color', second_task.data)
# self.assertNotIn('color', second_task.data)
self.assertIn('color', second_task.data)
self.assertIsNone(second_task.data['color']['value'])
def test_value_expression_with_default(self):