Merge pull request #94 from sartography/rrt/testing

Rrt/testing
This commit is contained in:
Aaron Louie 2020-06-01 01:25:53 -04:00 committed by GitHub
commit 58ffe741af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 815 additions and 405 deletions

20
Pipfile.lock generated
View File

@ -428,10 +428,10 @@
},
"mako": {
"hashes": [
"sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d",
"sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"
"sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27",
"sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"
],
"version": "==1.1.2"
"version": "==1.1.3"
},
"markupsafe": {
"hashes": [
@ -489,11 +489,11 @@
},
"marshmallow-sqlalchemy": {
"hashes": [
"sha256:3247e41e424146340b03a369f2b7c6f0364477ccedc4e2481e84d5f3a8d3c67f",
"sha256:dbbe51d28bb28e7ee2782e51310477f7a2c5a111a301f6dd8e264e11ab820427"
"sha256:03a555b610bb307689b821b64e2416593ec21a85925c8c436c2cd08ebc6bb85e",
"sha256:0ef59c8da8da2e18e808e3880158049e9d72f3031c84cc804b6c533a0eb668a9"
],
"index": "pypi",
"version": "==0.23.0"
"version": "==0.23.1"
},
"numpy": {
"hashes": [
@ -778,7 +778,7 @@
"spiffworkflow": {
"editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "c8d87826d496af825a184bdc3f0a751e603cfe44"
"ref": "b8a064a0bb76c705a1be04ee9bb8ac7beee56eb0"
},
"sqlalchemy": {
"hashes": [
@ -876,11 +876,11 @@
},
"xlsxwriter": {
"hashes": [
"sha256:488e1988ab16ff3a9cd58c7656d0a58f8abe46ee58b98eecea78c022db28656b",
"sha256:97ab487b81534415c5313154203f3e8a637d792b1e6a8201e8f7f71da0203c2a"
"sha256:828b3285fc95105f5b1946a6a015b31cf388bd5378fdc6604e4d1b7839df2e77",
"sha256:82a3b0e73e3913483da23791d1a25e4d2dbb3837d1be4129473526b9a270a5cc"
],
"index": "pypi",
"version": "==1.2.8"
"version": "==1.2.9"
},
"zipp": {
"hashes": [

View File

@ -173,6 +173,30 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Study"
/study/{study_id}/approvals:
parameters:
- name: study_id
in: path
required: true
description: The id of the study for which workflows should be returned.
schema:
type: integer
format: int32
get:
operationId: crc.api.approval.get_approvals_for_study
summary: Returns approvals for a single study
tags:
- Studies
- Approvals
responses:
'200':
description: An array of approvals
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Approval"
/workflow-specification:
get:
operationId: crc.api.workflow.all_specifications

View File

@ -5,15 +5,24 @@ from crc.models.approval import Approval, ApprovalModel, ApprovalSchema
from crc.services.approval_service import ApprovalService
def get_approvals(approver_uid = None):
def get_approvals(approver_uid=None):
if not approver_uid:
db_approvals = ApprovalService.get_all_approvals()
else:
db_approvals = ApprovalService.get_approvals_per_user(approver_uid)
approvals = [Approval.from_model(approval_model) for approval_model in db_approvals]
results = ApprovalSchema(many=True).dump(approvals)
return results
def get_approvals_for_study(study_id=None):
db_approvals = ApprovalService.get_approvals_for_study(study_id)
approvals = [Approval.from_model(approval_model) for approval_model in db_approvals]
results = ApprovalSchema(many=True).dump(approvals)
return results
def update_approval(approval_id, body):
if approval_id is None:
raise ApiError('unknown_approval', 'Please provide a valid Approval ID.')

View File

@ -1,3 +1,6 @@
from SpiffWorkflow import WorkflowException
from SpiffWorkflow.exceptions import WorkflowTaskExecException
from crc import ma, app
@ -34,6 +37,16 @@ class ApiError(Exception):
instance.file_name = task_spec._wf_spec.file
return instance
@classmethod
def from_workflow_exception(cls, code, message, exp: WorkflowException):
"""We catch a lot of workflow exception errors,
so consolidating the code, and doing the best things
we can with the data we have."""
if isinstance(exp, WorkflowTaskExecException):
return ApiError.from_task(code, message, exp.task)
else:
return ApiError.from_task_spec(code, message, exp.sender)
class ApiErrorSchema(ma.Schema):
class Meta:

View File

@ -12,8 +12,9 @@ from crc.services.file_service import FileService
def to_file_api(file_model):
"""Converts a FileModel object to something we can return via the aip"""
return File.from_models(file_model, FileService.get_file_data(file_model.id))
"""Converts a FileModel object to something we can return via the api"""
return File.from_models(file_model, FileService.get_file_data(file_model.id),
FileService.get_doc_dictionary())
def get_files(workflow_spec_id=None, workflow_id=None, form_field_key=None):

View File

@ -48,12 +48,10 @@ def update_study(study_id, body):
def get_study(study_id):
study_service = StudyService()
study = study_service.get_study(study_id)
study = StudyService.get_study(study_id)
if (study is None):
raise ApiError("Study not found", status_code=404)
schema = StudySchema()
return schema.dump(study)
return StudySchema().dump(study)
def delete_study(study_id):

View File

@ -44,6 +44,13 @@ def validate_workflow_specification(spec_id):
try:
WorkflowService.test_spec(spec_id)
except ApiError as ae:
ae.message = "When populating all fields ... " + ae.message
errors.append(ae)
try:
# Run the validation twice, the second time, just populate the required fields.
WorkflowService.test_spec(spec_id, required_only=True)
except ApiError as ae:
ae.message = "When populating only required fields ... " + ae.message
errors.append(ae)
return ApiErrorSchema(many=True).dump(errors)
@ -112,6 +119,8 @@ def __get_workflow_api_model(processor: WorkflowProcessor, next_task = None):
navigation.append(NavigationItem(**nav_item))
NavigationItemSchema().dump(nav_item)
spec = session.query(WorkflowSpecModel).filter_by(id=processor.workflow_spec_id).first()
workflow_api = WorkflowApi(
id=processor.get_workflow_id(),
status=processor.get_status(),
@ -122,7 +131,8 @@ def __get_workflow_api_model(processor: WorkflowProcessor, next_task = None):
is_latest_spec=processor.is_latest_spec,
total_tasks=processor.workflow_model.total_tasks,
completed_tasks=processor.workflow_model.completed_tasks,
last_updated=processor.workflow_model.last_updated
last_updated=processor.workflow_model.last_updated,
title=spec.display_name
)
if not next_task: # The Next Task can be requested to be a certain task, useful for parallel tasks.
# This may or may not work, sometimes there is no next task to complete.

View File

@ -36,6 +36,7 @@ class Task(object):
PROP_OPTIONS_VALUE_COLUMN = "spreadsheet.value.column"
PROP_OPTIONS_LABEL_COL = "spreadsheet.label.column"
PROP_LDAP_LOOKUP = "ldap.lookup"
VALIDATION_REQUIRED = "required"
FIELD_TYPE_AUTO_COMPLETE = "autocomplete"
@ -118,7 +119,7 @@ class NavigationItemSchema(ma.Schema):
class WorkflowApi(object):
def __init__(self, id, status, next_task, navigation,
spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, last_updated):
spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, last_updated, title):
self.id = id
self.status = status
self.next_task = next_task # The next task that requires user input.
@ -129,13 +130,14 @@ class WorkflowApi(object):
self.total_tasks = total_tasks
self.completed_tasks = completed_tasks
self.last_updated = last_updated
self.title = title
class WorkflowApiSchema(ma.Schema):
class Meta:
model = WorkflowApi
fields = ["id", "status", "next_task", "navigation",
"workflow_spec_id", "spec_version", "is_latest_spec", "total_tasks", "completed_tasks",
"last_updated"]
"last_updated", "title"]
unknown = INCLUDE
status = EnumField(WorkflowStatus)
@ -146,7 +148,7 @@ class WorkflowApiSchema(ma.Schema):
def make_workflow(self, data, **kwargs):
keys = ['id', 'status', 'next_task', 'navigation',
'workflow_spec_id', 'spec_version', 'is_latest_spec', "total_tasks", "completed_tasks",
"last_updated"]
"last_updated", "title"]
filtered_fields = {key: data[key] for key in keys}
filtered_fields['next_task'] = TaskSchema().make_task(data['next_task'])
return WorkflowApi(**filtered_fields)

View File

@ -11,10 +11,11 @@ from crc.models.file import FileDataModel
from crc.models.study import StudyModel
from crc.models.workflow import WorkflowModel
from crc.services.ldap_service import LdapService
from crc.services.file_service import FileService
class ApprovalStatus(enum.Enum):
WAITING = "WAITING" # no one has done jack.
PENDING = "PENDING" # no one has done jack.
APPROVED = "APPROVED" # approved by the reviewer
DECLINED = "DECLINED" # rejected by the reviewer
CANCELED = "CANCELED" # The document was replaced with a new version and this review is no longer needed.
@ -67,10 +68,10 @@ class Approval(object):
if model.study:
instance.title = model.study.title
principal_investigator_id = model.study.primary_investigator_id
instance.approver = {}
try:
ldap_service = LdapService()
principal_investigator_id = model.study.primary_investigator_id
user_info = ldap_service.user_info(principal_investigator_id)
except (ApiError, LDAPSocketOpenError) as exception:
user_info = None
@ -84,11 +85,25 @@ class Approval(object):
instance.approver['title'] = user_info.title
instance.approver['department'] = user_info.department
# TODO: Organize it properly, move it to services
doc_dictionary = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id'])
instance.associated_files = []
for approval_file in model.approval_files:
try:
extra_info = doc_dictionary[approval_file.file_data.file_model.irb_doc_code]
except:
extra_info = None
associated_file = {}
associated_file['id'] = approval_file.file_data.file_model.id
if extra_info:
irb_doc_code = approval_file.file_data.file_model.irb_doc_code
associated_file['name'] = '_'.join((irb_doc_code, approval_file.file_data.file_model.name))
associated_file['description'] = extra_info['description']
else:
associated_file['name'] = approval_file.file_data.file_model.name
associated_file['description'] = 'No description available'
associated_file['name'] = '(' + principal_investigator_id + ')' + associated_file['name']
associated_file['content_type'] = approval_file.file_data.file_model.content_type
instance.associated_files.append(associated_file)

View File

@ -86,7 +86,7 @@ class FileModel(db.Model):
class File(object):
@classmethod
def from_models(cls, model: FileModel, data_model: FileDataModel):
def from_models(cls, model: FileModel, data_model: FileDataModel, doc_dictionary):
instance = cls()
instance.id = model.id
instance.name = model.name
@ -99,6 +99,15 @@ class File(object):
instance.workflow_id = model.workflow_id
instance.irb_doc_code = model.irb_doc_code
instance.type = model.type
if model.irb_doc_code and model.irb_doc_code in doc_dictionary:
instance.category = "/".join(filter(None, [doc_dictionary[model.irb_doc_code]['category1'],
doc_dictionary[model.irb_doc_code]['category2'],
doc_dictionary[model.irb_doc_code]['category3']]))
instance.description = doc_dictionary[model.irb_doc_code]['description']
instance.download_name = ".".join([instance.category, model.type.value])
else:
instance.category = ""
instance.description = ""
if data_model:
instance.last_modified = data_model.date_created
instance.latest_version = data_model.version
@ -122,7 +131,8 @@ class FileSchema(ma.Schema):
model = File
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"]
"irb_doc_code", "last_modified", "latest_version", "type", "categories",
"description", "category", "description", "download_name"]
unknown = INCLUDE
type = EnumField(FileType)

View File

@ -5,7 +5,7 @@ from sqlalchemy import func
from crc import db, ma
from crc.api.common import ApiErrorSchema
from crc.models.file import FileModel, SimpleFileSchema
from crc.models.file import FileModel, SimpleFileSchema, FileSchema
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \
WorkflowModel
@ -106,7 +106,8 @@ class Study(object):
def __init__(self, title, last_updated, primary_investigator_id, user_uid,
id=None,
protocol_builder_status=None,
sponsor="", hsr_number="", ind_number="", categories=[], **argsv):
sponsor="", hsr_number="", ind_number="", categories=[],
files=[], approvals=[], **argsv):
self.id = id
self.user_uid = user_uid
self.title = title
@ -117,8 +118,9 @@ class Study(object):
self.hsr_number = hsr_number
self.ind_number = ind_number
self.categories = categories
self.approvals = approvals
self.warnings = []
self.files = []
self.files = files
@classmethod
def from_model(cls, study_model: StudyModel):
@ -149,12 +151,13 @@ class StudySchema(ma.Schema):
hsr_number = fields.String(allow_none=True)
sponsor = fields.String(allow_none=True)
ind_number = fields.String(allow_none=True)
files = fields.List(fields.Nested(SimpleFileSchema), dump_only=True)
files = fields.List(fields.Nested(FileSchema), dump_only=True)
approvals = fields.List(fields.Nested('ApprovalSchema'), dump_only=True)
class Meta:
model = Study
additional = ["id", "title", "last_updated", "primary_investigator_id", "user_uid",
"sponsor", "ind_number"]
"sponsor", "ind_number", "approvals", "files"]
unknown = INCLUDE
@marshmallow.post_load

View File

@ -19,6 +19,12 @@ class ApprovalService(object):
db_approvals = session.query(ApprovalModel).filter_by(approver_uid=approver_uid).all()
return db_approvals
@staticmethod
def get_approvals_for_study(study_id):
"""Returns a list of all approvals for the given study"""
db_approvals = session.query(ApprovalModel).filter_by(study_id=study_id).all()
return db_approvals
@staticmethod
def get_all_approvals():
"""Returns a list of all approvlas"""
@ -78,7 +84,7 @@ class ApprovalService(object):
version = 1
model = ApprovalModel(study_id=study_id, workflow_id=workflow_id,
approver_uid=approver_uid, status=ApprovalStatus.WAITING.value,
approver_uid=approver_uid, status=ApprovalStatus.PENDING.value,
message="", date_created=datetime.now(),
version=version)
approval_files = ApprovalService._create_approval_files(workflow_data_files, model)

View File

@ -22,6 +22,14 @@ class FileService(object):
DOCUMENT_LIST = "irb_documents.xlsx"
INVESTIGATOR_LIST = "investigators.xlsx"
__doc_dictionary = None
@staticmethod
def get_doc_dictionary():
if not FileService.__doc_dictionary:
FileService.__doc_dictionary = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id'])
return FileService.__doc_dictionary
@staticmethod
def add_workflow_spec_file(workflow_spec: WorkflowSpecModel,
name, content_type, binary_data, primary=False, is_status=False):

View File

@ -9,7 +9,7 @@ from ldap3.core.exceptions import LDAPSocketOpenError
from crc import db, session, app
from crc.api.common import ApiError
from crc.models.file import FileModel, FileModelSchema
from crc.models.file import FileModel, FileModelSchema, File
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
from crc.models.stats import TaskEventModel
from crc.models.study import StudyModel, Study, Category, WorkflowMetadata
@ -19,6 +19,8 @@ from crc.services.file_service import FileService
from crc.services.ldap_service import LdapService
from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.approval_service import ApprovalService
from crc.models.approval import Approval
class StudyService(object):
@ -54,7 +56,13 @@ class StudyService(object):
study = Study.from_model(study_model)
study.categories = StudyService.get_categories()
workflow_metas = StudyService.__get_workflow_metas(study_id)
study.files = FileService.get_files_for_study(study.id)
approvals = ApprovalService.get_approvals_for_study(study.id)
study.approvals = [Approval.from_model(approval_model) for approval_model in approvals]
files = FileService.get_files_for_study(study.id)
files = (File.from_models(model, FileService.get_file_data(model.id),
FileService.get_doc_dictionary()) for model in files)
study.files = list(files)
# Calling this line repeatedly is very very slow. It creates the
# master spec and runs it.
@ -175,6 +183,7 @@ class StudyService(object):
return documents
@staticmethod
def get_investigators(study_id):

View File

@ -299,21 +299,27 @@ class WorkflowProcessor(object):
return WorkflowStatus.waiting
def hard_reset(self):
"""Recreate this workflow, but keep the data from the last completed task and add it back into the first task.
This may be useful when a workflow specification changes, and users need to review all the
prior steps, but don't need to reenter all the previous data.
"""Recreate this workflow, but keep the data from the last completed task and add
it back into the first task. This may be useful when a workflow specification changes,
and users need to review all the prior steps, but they don't need to reenter all the previous data.
Returns the new version.
"""
# Create a new workflow based on the latest specs.
self.spec_data_files = FileService.get_spec_data_files(workflow_spec_id=self.workflow_spec_id)
spec = WorkflowProcessor.get_spec(self.spec_data_files, self.workflow_spec_id)
# spec = WorkflowProcessor.get_spec(self.workflow_spec_id, version)
bpmn_workflow = BpmnWorkflow(spec, script_engine=self._script_engine)
bpmn_workflow.data = self.bpmn_workflow.data
for task in bpmn_workflow.get_tasks(SpiffTask.READY):
task.data = self.bpmn_workflow.last_task.data
bpmn_workflow.do_engine_steps()
self.bpmn_workflow = bpmn_workflow
new_spec = WorkflowProcessor.get_spec(self.spec_data_files, self.workflow_spec_id)
new_bpmn_workflow = BpmnWorkflow(new_spec, script_engine=self._script_engine)
new_bpmn_workflow.data = self.bpmn_workflow.data
# Reset the current workflow to the beginning - which we will consider to be the first task after the root
# element. This feels a little sketchy, but I think it is safe to assume root will have one child.
first_task = self.bpmn_workflow.task_tree.children[0]
first_task.reset_token(reset_data=False)
for task in new_bpmn_workflow.get_tasks(SpiffTask.READY):
task.data = first_task.data
new_bpmn_workflow.do_engine_steps()
self.bpmn_workflow = new_bpmn_workflow
def get_status(self):
return self.status_of(self.bpmn_workflow)

View File

@ -7,9 +7,7 @@ from SpiffWorkflow import Task as SpiffTask, WorkflowException
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask
from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.camunda.specs.UserTask import EnumFormField
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.exceptions import WorkflowTaskExecException
from SpiffWorkflow.specs import CancelTask, StartTask
from flask import g
from jinja2 import Template
@ -69,23 +67,22 @@ class WorkflowService(object):
db.session.delete(user)
@staticmethod
def test_spec(spec_id):
def test_spec(spec_id, required_only=False):
"""Runs a spec through it's paces to see if it results in any errors.
Not fool-proof, but a good sanity check. Returns the final data
output form the last task if successful. """
output form the last task if successful.
required_only can be set to true, in which case this will run the
spec, only completing the required fields, rather than everything.
"""
workflow_model = WorkflowService.make_test_workflow(spec_id)
try:
processor = WorkflowProcessor(workflow_model, validate_only=True)
except WorkflowTaskExecException as wtee:
WorkflowService.delete_test_data()
raise ApiError.from_task("workflow_execution_exception", str(wtee),
wtee.task)
except WorkflowException as we:
WorkflowService.delete_test_data()
raise ApiError.from_task_spec("workflow_execution_exception", str(we),
we.sender)
raise ApiError.from_workflow_exception("workflow_execution_exception", str(we), we)
while not processor.bpmn_workflow.is_completed():
try:
@ -95,28 +92,26 @@ class WorkflowService(object):
task_api = WorkflowService.spiff_task_to_api_task(
task,
add_docs_and_forms=True) # Assure we try to process the documenation, and raise those errors.
WorkflowService.populate_form_with_random_data(task, task_api)
WorkflowService.populate_form_with_random_data(task, task_api, required_only)
task.complete()
except WorkflowTaskExecException as wtee:
WorkflowService.delete_test_data()
raise ApiError.from_task("workflow_execution_exception", str(wtee),
wtee.task)
except WorkflowException as we:
WorkflowService.delete_test_data()
raise ApiError.from_task_spec("workflow_execution_exception", str(we),
we.sender)
raise ApiError.from_workflow_exception("workflow_execution_exception", str(we), we)
WorkflowService.delete_test_data()
return processor.bpmn_workflow.last_task.data
@staticmethod
def populate_form_with_random_data(task, task_api):
def populate_form_with_random_data(task, task_api, required_only):
"""populates a task with random data - useful for testing a spec."""
if not hasattr(task.task_spec, 'form'): return
form_data = {}
for field in task_api.form.fields:
if required_only and (not field.has_validation(Task.VALIDATION_REQUIRED) or
field.get_validation(Task.VALIDATION_REQUIRED).lower().strip() != "true"):
continue # Don't include any fields that aren't specifically marked as required.
if field.has_property(Task.PROP_OPTIONS_REPEAT):
group = field.get_property(Task.PROP_OPTIONS_REPEAT)
if group not in form_data:
@ -299,10 +294,11 @@ class WorkflowService(object):
template = Template(raw_doc)
return template.render(**spiff_task.data)
except jinja2.exceptions.TemplateError as ue:
# return "Error processing template. %s" % ue.message
raise ApiError(code="template_error", message="Error processing template for task %s: %s" %
(spiff_task.task_spec.name, str(ue)), status_code=500)
except TypeError as te:
raise ApiError(code="template_error", message="Error processing template for task %s: %s" %
(spiff_task.task_spec.name, str(te)), status_code=500)
# TODO: Catch additional errors and report back.
@staticmethod

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_06veek1" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<decision id="Decision_ExclusiveAMCheck" name="Exclusive AM Check">
<decisionTable id="decisionTable_1">
<input id="InputClause_1z0jy2o" label="How Many Exclusive Spaces?">
<inputExpression id="LiteralExpression_0tvij2j" typeRef="integer" expressionLanguage="python">
<text>'exclusive' in locals() and len(exclusive)</text>
</inputExpression>
</input>
<input id="input_1" label="Number Without Area Monitor">
<inputExpression id="inputExpression_1" typeRef="integer" expressionLanguage="python">
<text>sum([1 for x in exclusive if x.get('ExclusiveSpaceAMComputingID',None) == None])</text>
</inputExpression>
</input>
<output id="output_1" label="All Possible Area Monitors Entered" name="isAllExclusiveAreaMonitors" typeRef="boolean" />
<rule id="DecisionRule_07162mr">
<description>No exclusive spaces without Area Monitor</description>
<inputEntry id="UnaryTests_1892rx8">
<text>&gt;0</text>
</inputEntry>
<inputEntry id="UnaryTests_1jqxc3u">
<text>0</text>
</inputEntry>
<outputEntry id="LiteralExpression_16l50ps">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0ifa4wu">
<description>One or more exclusive space without an Area Monitor</description>
<inputEntry id="UnaryTests_1jakyab">
<text>&gt;0</text>
</inputEntry>
<inputEntry id="UnaryTests_0szbwxc">
<text>&gt; 0</text>
</inputEntry>
<outputEntry id="LiteralExpression_0td8sa6">
<text>false</text>
</outputEntry>
</rule>
<rule id="DecisionRule_026r0im">
<description>No exclusive spaces entered</description>
<inputEntry id="UnaryTests_0c670b6">
<text>0</text>
</inputEntry>
<inputEntry id="UnaryTests_0j06ysc">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1apwzvv">
<text>true</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_06veek1" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<decision id="Decision_SharedAMCheck" name="Shared AM Check">
<decisionTable id="decisionTable_1">
<input id="InputClause_1koybx6" label="How Many Shared Spaces">
<inputExpression id="LiteralExpression_1mjo0y4" typeRef="integer" expressionLanguage="python">
<text>'shared' in locals() and len(shared)</text>
</inputExpression>
</input>
<input id="input_1" label="Number Without Area Monitor">
<inputExpression id="inputExpression_1" typeRef="integer" expressionLanguage="python">
<text>sum([1 for x in exclusive if x.get('SharedSpaceAMComputingID',None) == None])</text>
</inputExpression>
</input>
<output id="output_1" label="All Possible Shared Area Monitors Entered" name="isAllSharedAreaMonitors" typeRef="boolean" />
<rule id="DecisionRule_07162mr">
<description>No shared spaces without Area Monitor</description>
<inputEntry id="UnaryTests_1p4ab2l">
<text>&gt;0</text>
</inputEntry>
<inputEntry id="UnaryTests_1jqxc3u">
<text>0</text>
</inputEntry>
<outputEntry id="LiteralExpression_16l50ps">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0ifa4wu">
<description>One or more shared space without an Area Monitor</description>
<inputEntry id="UnaryTests_06bujee">
<text>&gt;0</text>
</inputEntry>
<inputEntry id="UnaryTests_0szbwxc">
<text>&gt; 0</text>
</inputEntry>
<outputEntry id="LiteralExpression_0td8sa6">
<text>false</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1uh85sk">
<description>No shared spaces entered</description>
<inputEntry id="UnaryTests_15grk62">
<text>0</text>
</inputEntry>
<inputEntry id="UnaryTests_1gaiomm">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1iep8ai">
<text>true</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -115,15 +115,17 @@ class BaseTest(unittest.TestCase):
self.assertIsNotNone(user_model.display_name)
return dict(Authorization='Bearer ' + user_model.encode_auth_token().decode())
def load_example_data(self, use_crc_data=False):
def load_example_data(self, use_crc_data=False, use_rrt_data=False):
"""use_crc_data will cause this to load the mammoth collection of documents
we built up developing crc, otherwise it depends on a small setup for
running tests."""
we built up developing crc, use_rrt_data will do the same for hte rrt project,
otherwise it depends on a small setup for running tests."""
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
if(use_crc_data):
if use_crc_data:
ExampleDataLoader().load_all()
elif use_rrt_data:
ExampleDataLoader().load_rrt()
else:
ExampleDataLoader().load_test_data()

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_1elv5t1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<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_1elv5t1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_15vbyda" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1ma1wxb</bpmn:outgoing>
@ -8,7 +8,11 @@
<bpmn:userTask id="get_num_presents" name="Get number of presents" camunda:formKey="present_question">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="num_presents" label="How many presents will my dog Ginger leave for me today?" type="long" defaultValue="0" />
<camunda:formField id="num_presents" label="How many presents will my dog Ginger leave for me today?" type="long" defaultValue="0">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1ma1wxb</bpmn:incoming>
@ -26,38 +30,37 @@ Based on the information you provided (Ginger left {{num_presents}}, we recommen
## {{message}}
We hope you both have an excellent day!
</bpmn:documentation>
We hope you both have an excellent day!</bpmn:documentation>
<bpmn:incoming>SequenceFlow_0grui6f</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_0grui6f" sourceRef="Task_0sgafty" targetRef="EndEvent_0tsqkyu" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_15vbyda">
<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_1ma1wxb_di" bpmnElement="SequenceFlow_1ma1wxb">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
<bpmndi:BPMNEdge id="SequenceFlow_0grui6f_di" bpmnElement="SequenceFlow_0grui6f">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_15w5gb3_di" bpmnElement="get_num_presents">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1uxaqwp_di" bpmnElement="SequenceFlow_1uxaqwp">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1ma1wxb_di" bpmnElement="SequenceFlow_1ma1wxb">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" 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="UserTask_15w5gb3_di" bpmnElement="get_num_presents">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_10c5wgr_di" bpmnElement="Task_0sgafty">
<dc:Bounds x="430" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0tsqkyu_di" bpmnElement="EndEvent_0tsqkyu">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0grui6f_di" bpmnElement="SequenceFlow_0grui6f">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -8,7 +8,11 @@
<bpmn:userTask id="Task_Has_Bananas" name="Enter Do You Have Bananas" camunda:formKey="bananas_form">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="has_bananas" label="Do you have bananas?" type="boolean" />
<camunda:formField id="has_bananas" label="Do you have bananas?" type="boolean">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1pnq3kg</bpmn:incoming>

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_1gjhqt9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
<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_1gjhqt9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_1ds61df" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0c7wlth</bpmn:outgoing>
@ -108,6 +108,9 @@ Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="type" label="Type" type="enum" defaultValue="cat">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="norris" name="Chuck Norris" />
<camunda:value id="cat" name="Cat Fact" />
<camunda:value id="buzzword" name="Business Buzzword" />
@ -121,8 +124,7 @@ Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
<bpmn:outgoing>SequenceFlow_0641sh6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:scriptTask id="Task_Get_Fact_From_API" name="Display Fact">
<bpmn:documentation>
</bpmn:documentation>
<bpmn:documentation />
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:inputParameter name="Fact.type" />
@ -155,6 +157,18 @@ Your random fact is:
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1ds61df">
<bpmndi:BPMNEdge id="SequenceFlow_0t29gjo_di" bpmnElement="SequenceFlow_0t29gjo">
<di:waypoint x="570" y="250" />
<di:waypoint x="692" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0641sh6_di" bpmnElement="SequenceFlow_0641sh6">
<di:waypoint x="370" y="250" />
<di:waypoint x="470" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0c7wlth_di" bpmnElement="SequenceFlow_0c7wlth">
<di:waypoint x="188" y="250" />
<di:waypoint x="270" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
@ -164,35 +178,23 @@ Your random fact is:
<bpmndi:BPMNShape id="ScriptTask_10keafb_di" bpmnElement="Task_Get_Fact_From_API">
<dc:Bounds x="470" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
<dc:Bounds x="692" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_09fq7kh_di" bpmnElement="TextAnnotation_09fq7kh">
<dc:Bounds x="330" y="116" width="99.99202297383536" height="68.28334396936822" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1234e5n_di" bpmnElement="TextAnnotation_1234e5n">
<dc:Bounds x="570" y="120" width="100" height="68" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1cfasjp_di" bpmnElement="Association_1cfasjp">
<di:waypoint x="344" y="210" />
<di:waypoint x="359" y="184" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="TextAnnotation_1234e5n_di" bpmnElement="TextAnnotation_1234e5n">
<dc:Bounds x="570" y="120" width="100" height="68" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1qirnyy_di" bpmnElement="Association_1qirnyy">
<di:waypoint x="561" y="210" />
<di:waypoint x="584" y="188" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_0u1cgrf_di" bpmnElement="EndEvent_0u1cgrf">
<dc:Bounds x="692" y="232" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0c7wlth_di" bpmnElement="SequenceFlow_0c7wlth">
<di:waypoint x="188" y="250" />
<di:waypoint x="270" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0641sh6_di" bpmnElement="SequenceFlow_0641sh6">
<di:waypoint x="370" y="250" />
<di:waypoint x="470" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0t29gjo_di" bpmnElement="SequenceFlow_0t29gjo">
<di:waypoint x="570" y="250" />
<di:waypoint x="692" y="250" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,48 @@
<?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_1v1rp1q" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Required" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_0lvudp8</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_0lvudp8" sourceRef="StartEvent_1" targetRef="Task_Required_Fields" />
<bpmn:endEvent id="EndEvent_0q4qzl9">
<bpmn:incoming>SequenceFlow_02vev7n</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="SequenceFlow_02vev7n" sourceRef="Task_Required_Fields" targetRef="EndEvent_0q4qzl9" />
<bpmn:userTask id="Task_Required_Fields" name="Required fields" camunda:formKey="RequiredForm">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="string_required" label="String" type="string" defaultValue="some string">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="string_not_required" type="string" defaultValue="If ya like, I don&#39;t care." />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0lvudp8</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_02vev7n</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Required">
<bpmndi:BPMNEdge id="SequenceFlow_02vev7n_di" bpmnElement="SequenceFlow_02vev7n">
<di:waypoint x="370" y="117" />
<di:waypoint x="432" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0lvudp8_di" bpmnElement="SequenceFlow_0lvudp8">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" 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="EndEvent_0q4qzl9_di" bpmnElement="EndEvent_0q4qzl9">
<dc:Bounds x="432" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_18ly1yq_di" bpmnElement="Task_Required_Fields">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -45,7 +45,7 @@ class TestApprovals(BaseTest):
study=self.study,
workflow=self.workflow,
approver_uid='arc93',
status=ApprovalStatus.WAITING.value,
status=ApprovalStatus.PENDING.value,
version=1
)
session.add(self.approval)
@ -54,7 +54,7 @@ class TestApprovals(BaseTest):
study=self.study,
workflow=self.workflow,
approver_uid='dhf8r',
status=ApprovalStatus.WAITING.value,
status=ApprovalStatus.PENDING.value,
version=1
)
session.add(self.approval_2)
@ -98,7 +98,7 @@ class TestApprovals(BaseTest):
data = dict(APPROVAL_PAYLOAD)
data['id'] = approval_id
self.assertEqual(self.approval.status, ApprovalStatus.WAITING.value)
self.assertEqual(self.approval.status, ApprovalStatus.PENDING.value)
rv = self.app.put(f'/v1.0/approval/{approval_id}',
content_type="application/json",

View File

@ -1,5 +1,6 @@
import json
from tests.base_test import BaseTest
from datetime import datetime, timezone
from unittest.mock import patch
@ -8,8 +9,9 @@ from crc.models.protocol_builder import ProtocolBuilderStatus, \
ProtocolBuilderStudySchema
from crc.models.stats import TaskEventModel
from crc.models.study import StudyModel, StudySchema
from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecCategoryModel
from crc.services.protocol_builder import ProtocolBuilderService
from crc.models.workflow import WorkflowSpecModel, WorkflowModel
from crc.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor
class TestStudyApi(BaseTest):
@ -68,6 +70,34 @@ class TestStudyApi(BaseTest):
self.assertEqual(0, workflow["total_tasks"])
self.assertEqual(0, workflow["completed_tasks"])
def test_get_study_has_details_about_files(self):
# Set up the study and attach a file to it.
self.load_example_data()
self.create_reference_document()
workflow = self.create_workflow('file_upload_form')
processor = WorkflowProcessor(workflow)
task = processor.next_task()
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_workflow_file(workflow_id=workflow.id,
name="anything.png", content_type="png",
binary_data=b'1234', irb_doc_code=irb_code)
api_response = self.app.get('/v1.0/study/%i' % workflow.study_id,
headers=self.logged_in_headers(), content_type="application/json")
self.assert_success(api_response)
study = StudySchema().loads(api_response.get_data(as_text=True))
self.assertEquals(1, len(study.files))
self.assertEquals("UVA Compliance/PRC Approval", study.files[0]["category"])
self.assertEquals("Cancer Center's PRC Approval Form", study.files[0]["description"])
self.assertEquals("UVA Compliance/PRC Approval.png", study.files[0]["download_name"])
# TODO: WRITE A TEST FOR STUDY FILES
def test_get_study_has_details_about_approvals(self):
# TODO: WRITE A TEST FOR STUDY APPROVALS
pass
def test_add_study(self):
self.load_example_data()
study = self.add_test_study()

View File

@ -25,7 +25,7 @@ class TestWorkflowProcessor(BaseTest):
def _populate_form_with_random_data(self, task):
api_task = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True)
WorkflowService.populate_form_with_random_data(task, api_task)
WorkflowService.populate_form_with_random_data(task, api_task, required_only=False)
def get_processor(self, study_model, spec_model):
workflow_model = StudyService._create_workflow_model(study_model, spec_model)

View File

@ -77,5 +77,5 @@ class TestWorkflowService(BaseTest):
processor.do_engine_steps()
task = processor.next_task()
task_api = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True)
WorkflowService.populate_form_with_random_data(task, task_api)
WorkflowService.populate_form_with_random_data(task, task_api, required_only=False)
self.assertTrue(isinstance(task.data["sponsor"], dict))

View File

@ -49,6 +49,13 @@ class TestWorkflowSpecValidation(BaseTest):
self.load_example_data(use_crc_data=True)
app.config['PB_ENABLED'] = True
self.validate_all_loaded_workflows()
def test_successful_validation_of_rrt_workflows(self):
self.load_example_data(use_rrt_data=True)
self.validate_all_loaded_workflows()
def validate_all_loaded_workflows(self):
workflows = session.query(WorkflowSpecModel).all()
errors = []
for w in workflows:
@ -59,15 +66,16 @@ class TestWorkflowSpecValidation(BaseTest):
errors.extend(ApiErrorSchema(many=True).load(json_data))
self.assertEqual(0, len(errors), json.dumps(errors))
def test_invalid_expression(self):
self.load_example_data()
errors = self.validate_workflow("invalid_expression")
self.assertEqual(1, len(errors))
self.assertEqual(2, len(errors))
self.assertEqual("workflow_execution_exception", errors[0]['code'])
self.assertEqual("ExclusiveGateway_003amsm", errors[0]['task_id'])
self.assertEqual("Has Bananas Gateway", errors[0]['task_name'])
self.assertEqual("invalid_expression.bpmn", errors[0]['file_name'])
self.assertEqual('ExclusiveGateway_003amsm: Error evaluating expression \'this_value_does_not_exist==true\', '
self.assertEqual('When populating all fields ... ExclusiveGateway_003amsm: Error evaluating expression \'this_value_does_not_exist==true\', '
'name \'this_value_does_not_exist\' is not defined', errors[0]["message"])
self.assertIsNotNone(errors[0]['task_data'])
self.assertIn("has_bananas", errors[0]['task_data'])
@ -75,7 +83,7 @@ class TestWorkflowSpecValidation(BaseTest):
def test_validation_error(self):
self.load_example_data()
errors = self.validate_workflow("invalid_spec")
self.assertEqual(1, len(errors))
self.assertEqual(2, len(errors))
self.assertEqual("workflow_validation_error", errors[0]['code'])
self.assertEqual("StartEvent_1", errors[0]['task_id'])
self.assertEqual("invalid_spec.bpmn", errors[0]['file_name'])
@ -83,7 +91,7 @@ class TestWorkflowSpecValidation(BaseTest):
def test_invalid_script(self):
self.load_example_data()
errors = self.validate_workflow("invalid_script")
self.assertEqual(1, len(errors))
self.assertEqual(2, len(errors))
self.assertEqual("workflow_execution_exception", errors[0]['code'])
self.assertTrue("NoSuchScript" in errors[0]['message'])
self.assertEqual("Invalid_Script_Task", errors[0]['task_id'])
@ -96,3 +104,16 @@ class TestWorkflowSpecValidation(BaseTest):
final_data = WorkflowService.test_spec(spec_model.id)
self.assertIsNotNone(final_data)
self.assertIn('cats', final_data)
def test_required_fields(self):
self.load_example_data()
spec_model = self.load_test_spec('required_fields')
final_data = WorkflowService.test_spec(spec_model.id)
self.assertIsNotNone(final_data)
self.assertIn('string_required', final_data)
self.assertIn('string_not_required', final_data)
final_data = WorkflowService.test_spec(spec_model.id, required_only=True)
self.assertIsNotNone(final_data)
self.assertIn('string_required', final_data)
self.assertNotIn('string_not_required', final_data)