diff --git a/Pipfile.lock b/Pipfile.lock index 37672182..909cf764 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -319,10 +319,11 @@ }, "flask-sqlalchemy": { "hashes": [ - "sha256:0b656fbf87c5f24109d859bafa791d29751fabbda2302b606881ae5485b557a5", - "sha256:fcfe6df52cd2ed8a63008ca36b86a51fa7a4b70cef1c39e5625f722fca32308e" + "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", + "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" ], - "version": "==2.4.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.4" }, "future": { "hashes": [ @@ -411,8 +412,12 @@ }, "ldap3": { "hashes": [ - "sha256:17f04298b70bf7ecaa5db8a7d8622b5a962ef7fc2b245b2eea705ac1c24338c0", - "sha256:81df4ac8b6df10fb1f05b17c18d0cb8c4c344d5a03083c382824960ed959cf5b" + "sha256:298769ab0232b3a3efa1e84881096c24526fe37911c83a11285f222fe4975efd", + "sha256:81df4ac8b6df10fb1f05b17c18d0cb8c4c344d5a03083c382824960ed959cf5b", + "sha256:53aaae5bf14f3827c69600ddf4d61b88f49c055bb93060e9702c5bafd206c744", + "sha256:4fd2db72d0412cc16ee86be01332095e86e361329c3579b314231eb2e56c7871", + "sha256:52ab557b3c4908db4a90bea16731aa714b1b54e039b54fd4c4b83994c6c48c0c", + "sha256:17f04298b70bf7ecaa5db8a7d8622b5a962ef7fc2b245b2eea705ac1c24338c0" ], "index": "pypi", "version": "==2.7" @@ -641,8 +646,19 @@ }, "pyasn1": { "hashes": [ + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12" ], "version": "==0.4.8" }, @@ -697,9 +713,11 @@ }, "python-editor": { "hashes": [ - "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", + "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", - "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" + "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", + "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522", + "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d" ], "version": "==1.0.4" }, @@ -882,7 +900,7 @@ "spiffworkflow": { "editable": true, "git": "https://github.com/sartography/SpiffWorkflow.git", - "ref": "bf9fdcd51846126e0acc8eeccad1a16c8b8330ce" + "ref": "c72ced41e323aa69fcb6f7708e1869e98add716d" }, "sqlalchemy": { "hashes": [ diff --git a/config/default.py b/config/default.py index ed44e6fe..5c8f8c51 100644 --- a/config/default.py +++ b/config/default.py @@ -30,7 +30,7 @@ SQLALCHEMY_DATABASE_URI = environ.get( default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME) ) TOKEN_AUTH_TTL_HOURS = float(environ.get('TOKEN_AUTH_TTL_HOURS', default=24)) -TOKEN_AUTH_SECRET_KEY = environ.get('TOKEN_AUTH_SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.") +SECRET_KEY = environ.get('SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.") FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session") SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER") diff --git a/config/testing.py b/config/testing.py index c7a777ad..5b03cc41 100644 --- a/config/testing.py +++ b/config/testing.py @@ -5,7 +5,7 @@ basedir = os.path.abspath(os.path.dirname(__file__)) NAME = "CR Connect Workflow" TESTING = True -TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod." +SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod." # This is here, for when we are running the E2E Tests in the frontend code bases. # which will set the TESTING envronment to true, causing this to execute, but we need diff --git a/crc/models/api_models.py b/crc/models/api_models.py index 8a1d3082..843609e0 100644 --- a/crc/models/api_models.py +++ b/crc/models/api_models.py @@ -33,15 +33,36 @@ class NavigationItem(object): class Task(object): + ########################################################################## + # Custom properties and validations defined in Camunda form fields # + ########################################################################## + + # Repeating form section PROP_OPTIONS_REPEAT = "repeat" - PROP_OPTIONS_FILE = "spreadsheet.name" - PROP_OPTIONS_VALUE_COLUMN = "spreadsheet.value.column" - PROP_OPTIONS_LABEL_COL = "spreadsheet.label.column" + + # Read-only field PROP_OPTIONS_READ_ONLY = "read_only" + + # LDAP lookup PROP_LDAP_LOOKUP = "ldap.lookup" - VALIDATION_REQUIRED = "required" + + # Autocomplete field FIELD_TYPE_AUTO_COMPLETE = "autocomplete" + # Required field + VALIDATION_REQUIRED = "required" + + # Enum field options values pulled from a spreadsheet + PROP_OPTIONS_FILE_NAME = "spreadsheet.name" + PROP_OPTIONS_FILE_VALUE_COLUMN = "spreadsheet.value.column" + PROP_OPTIONS_FILE_LABEL_COLUMN = "spreadsheet.label.column" + + # Enum field options values pulled from task data + PROP_OPTIONS_DATA_NAME = "data.name" + PROP_OPTIONS_DATA_VALUE_COLUMN = "data.value.column" + PROP_OPTIONS_DATA_LABEL_COLUMN = "data.label.column" + + ########################################################################## def __init__(self, id, name, title, type, state, lane, form, documentation, data, multi_instance_type, multi_instance_count, multi_instance_index, diff --git a/crc/models/file.py b/crc/models/file.py index 5eb50d4e..8afed6cd 100644 --- a/crc/models/file.py +++ b/crc/models/file.py @@ -144,7 +144,6 @@ class LookupFileModel(db.Model): """Gives us a quick way to tell what kind of lookup is set on a form field. Connected to the file data model, so that if a new version of the same file is created, we can update the listing.""" - #fixme: What happens if they change the file associated with a lookup field? __tablename__ = 'lookup_file' id = db.Column(db.Integer, primary_key=True) workflow_spec_id = db.Column(db.String) diff --git a/crc/models/user.py b/crc/models/user.py index 55bba35f..221176bc 100644 --- a/crc/models/user.py +++ b/crc/models/user.py @@ -35,7 +35,7 @@ class UserModel(db.Model): } return jwt.encode( payload, - app.config.get('TOKEN_AUTH_SECRET_KEY'), + app.config.get('SECRET_KEY'), algorithm='HS256', ) @@ -47,7 +47,7 @@ class UserModel(db.Model): :return: integer|string """ try: - payload = jwt.decode(auth_token, app.config.get('TOKEN_AUTH_SECRET_KEY'), algorithms='HS256') + payload = jwt.decode(auth_token, app.config.get('SECRET_KEY'), algorithms='HS256') return payload except jwt.ExpiredSignatureError: raise ApiError('token_expired', 'The Authentication token you provided expired and must be renewed.') diff --git a/crc/scripts/study_info.py b/crc/scripts/study_info.py index 94e35249..f274b899 100644 --- a/crc/scripts/study_info.py +++ b/crc/scripts/study_info.py @@ -192,7 +192,7 @@ Returns information specific to the protocol. "workflow_spec_id": "irb_api_details", }, 'protocol': { - id: 0, + 'id': 0, } } } diff --git a/crc/services/lookup_service.py b/crc/services/lookup_service.py index 8e849085..c9eb1dd8 100644 --- a/crc/services/lookup_service.py +++ b/crc/services/lookup_service.py @@ -4,7 +4,7 @@ from collections import OrderedDict import pandas as pd from pandas import ExcelFile, np -from sqlalchemy import func, desc +from sqlalchemy import desc from sqlalchemy.sql.functions import GenericFunction from crc import db @@ -76,11 +76,12 @@ class LookupService(object): @staticmethod def create_lookup_model(workflow_model, field_id): """ - This is all really expensive, but should happen just once (per file change). - Checks to see if the options are provided in a separate lookup table associated with the - workflow, and if so, assures that data exists in the database, and return a model than can be used - to locate that data. - Returns: an array of LookupData, suitable for returning to the api. + This is all really expensive, but should happen just once (per file change). + + Checks to see if the options are provided in a separate lookup table associated with the workflow, and if so, + assures that data exists in the database, and return a model than can be used to locate that data. + + Returns: an array of LookupData, suitable for returning to the API. """ processor = WorkflowProcessor(workflow_model) # VERY expensive, Ludicrous for lookup / type ahead spiff_task, field = processor.find_task_and_field_by_field_id(field_id) @@ -92,21 +93,21 @@ class LookupService(object): for model in existing_models: # Do it one at a time to cause the required cascade of deletes. db.session.delete(model) - - if field.has_property(Task.PROP_OPTIONS_FILE): - if not field.has_property(Task.PROP_OPTIONS_VALUE_COLUMN) or \ - not field.has_property(Task.PROP_OPTIONS_LABEL_COL): - raise ApiError.from_task("invalid_emum", + # Use the contents of a file to populate enum field options + if field.has_property(Task.PROP_OPTIONS_FILE_NAME): + if not (field.has_property(Task.PROP_OPTIONS_FILE_VALUE_COLUMN) or + field.has_property(Task.PROP_OPTIONS_FILE_LABEL_COLUMN)): + raise ApiError.from_task("invalid_enum", "For enumerations based on an xls file, you must include 3 properties: %s, " - "%s, and %s" % (Task.PROP_OPTIONS_FILE, - Task.PROP_OPTIONS_VALUE_COLUMN, - Task.PROP_OPTIONS_LABEL_COL), + "%s, and %s" % (Task.PROP_OPTIONS_FILE_NAME, + Task.PROP_OPTIONS_FILE_VALUE_COLUMN, + Task.PROP_OPTIONS_FILE_LABEL_COLUMN), task=spiff_task) # Get the file data from the File Service - file_name = field.get_property(Task.PROP_OPTIONS_FILE) - value_column = field.get_property(Task.PROP_OPTIONS_VALUE_COLUMN) - label_column = field.get_property(Task.PROP_OPTIONS_LABEL_COL) + file_name = field.get_property(Task.PROP_OPTIONS_FILE_NAME) + value_column = field.get_property(Task.PROP_OPTIONS_FILE_VALUE_COLUMN) + label_column = field.get_property(Task.PROP_OPTIONS_FILE_LABEL_COLUMN) latest_files = FileService.get_spec_data_files(workflow_spec_id=workflow_model.workflow_spec_id, workflow_id=workflow_model.id, name=file_name) @@ -118,14 +119,15 @@ class LookupService(object): lookup_model = LookupService.build_lookup_table(data_model, value_column, label_column, workflow_model.workflow_spec_id, field_id) + # Use the results of an LDAP request to populate enum field options elif field.has_property(Task.PROP_LDAP_LOOKUP): lookup_model = LookupFileModel(workflow_spec_id=workflow_model.workflow_spec_id, field_id=field_id, is_ldap=True) else: raise ApiError("unknown_lookup_option", - "Lookup supports using spreadsheet options or ldap options, and neither " - "was provided.") + "Lookup supports using spreadsheet or LDAP options, " + "and neither of those was provided.") db.session.add(lookup_model) db.session.commit() return lookup_model @@ -140,11 +142,11 @@ class LookupService(object): df = xls.parse(xls.sheet_names[0]) # Currently we only look at the fist sheet. df = pd.DataFrame(df).replace({np.nan: None}) if value_column not in df: - raise ApiError("invalid_emum", + raise ApiError("invalid_enum", "The file %s does not contain a column named % s" % (data_model.file_model.name, value_column)) if label_column not in df: - raise ApiError("invalid_emum", + raise ApiError("invalid_enum", "The file %s does not contain a column named % s" % (data_model.file_model.name, label_column)) @@ -211,3 +213,4 @@ class LookupService(object): "data": user }) return user_list + diff --git a/crc/services/workflow_processor.py b/crc/services/workflow_processor.py index 50736e5f..60040a95 100644 --- a/crc/services/workflow_processor.py +++ b/crc/services/workflow_processor.py @@ -167,7 +167,10 @@ class WorkflowProcessor(object): bpmn_workflow = BpmnWorkflow(spec, script_engine=self._script_engine) bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = workflow_model.study_id bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = validate_only - bpmn_workflow.do_engine_steps() + try: + bpmn_workflow.do_engine_steps() + except WorkflowException as we: + raise ApiError.from_task_spec("error_loading_workflow", str(we), we.sender) return bpmn_workflow def save(self): @@ -308,7 +311,10 @@ class WorkflowProcessor(object): 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 - new_bpmn_workflow.do_engine_steps() + try: + new_bpmn_workflow.do_engine_steps() + except WorkflowException as we: + raise ApiError.from_task_spec("hard_reset_engine_steps_error", str(we), we.sender) self.bpmn_workflow = new_bpmn_workflow def get_status(self): diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index 09ea68c1..74d70408 100644 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -7,9 +7,11 @@ import random import jinja2 from SpiffWorkflow import Task as SpiffTask, WorkflowException +from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask from SpiffWorkflow.bpmn.specs.MultiInstanceTask import MultiInstanceTask from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask +from SpiffWorkflow.bpmn.specs.StartEvent import StartEvent from SpiffWorkflow.bpmn.specs.UserTask import UserTask from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask from SpiffWorkflow.specs import CancelTask, StartTask @@ -104,7 +106,7 @@ class WorkflowService(object): f" a unique user id.", task) 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. + add_docs_and_forms=True) # Assure we try to process the documentation, and raise those errors. WorkflowService.populate_form_with_random_data(task, task_api, required_only) processor.complete_task(task) except WorkflowException as we: @@ -290,20 +292,14 @@ class WorkflowService(object): def spiff_task_to_api_task(spiff_task, add_docs_and_forms=False): task_type = spiff_task.task_spec.__class__.__name__ - if isinstance(spiff_task.task_spec, UserTask): - task_type = "UserTask" - elif isinstance(spiff_task.task_spec, ManualTask): - task_type = "ManualTask" - elif isinstance(spiff_task.task_spec, BusinessRuleTask): - task_type = "BusinessRuleTask" - elif isinstance(spiff_task.task_spec, CancelTask): - task_type = "CancelTask" - elif isinstance(spiff_task.task_spec, ScriptTask): - task_type = "ScriptTask" - elif isinstance(spiff_task.task_spec, StartTask): - task_type = "StartTask" - else: - task_type = "NoneTask" + task_types = [UserTask, ManualTask, BusinessRuleTask, CancelTask, ScriptTask, StartTask, EndEvent, StartEvent] + + for t in task_types: + if isinstance(spiff_task.task_spec, t): + task_type = t.__name__ + break + else: + task_type = "NoneTask" info = spiff_task.task_info() if info["is_looping"]: @@ -417,16 +413,39 @@ class WorkflowService(object): # If this is an auto-complete field, do not populate options, a lookup will happen later. if field.type == Task.FIELD_TYPE_AUTO_COMPLETE: pass - elif field.has_property(Task.PROP_OPTIONS_FILE): + elif field.has_property(Task.PROP_OPTIONS_FILE_NAME): lookup_model = LookupService.get_lookup_model(spiff_task, field) data = db.session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_model).all() if not hasattr(field, 'options'): field.options = [] for d in data: field.options.append({"id": d.value, "name": d.label, "data": d.data}) - + elif field.has_property(Task.PROP_OPTIONS_DATA_NAME): + field.options = WorkflowService.get_options_from_task_data(spiff_task, field) return field + @staticmethod + def get_options_from_task_data(spiff_task, field): + if not (field.has_property(Task.PROP_OPTIONS_DATA_VALUE_COLUMN) or + field.has_property(Task.PROP_OPTIONS_DATA_LABEL_COLUMN)): + raise ApiError.from_task("invalid_enum", + f"For enumerations based on task data, you must include 3 properties: " + f"{Task.PROP_OPTIONS_DATA_NAME}, {Task.PROP_OPTIONS_DATA_VALUE_COLUMN}, " + f"{Task.PROP_OPTIONS_DATA_LABEL_COLUMN}", task=spiff_task) + prop = field.get_property(Task.PROP_OPTIONS_DATA_NAME) + if prop not in spiff_task.data: + raise ApiError.from_task("invalid_enum", f"For enumerations based on task data, task data must have " + f"a property called {prop}", task=spiff_task) + # Get the enum options from the task data + data_model = spiff_task.data[prop] + value_column = field.get_property(Task.PROP_OPTIONS_DATA_VALUE_COLUMN) + label_column = field.get_property(Task.PROP_OPTIONS_DATA_LABEL_COLUMN) + items = data_model.items() if isinstance(data_model, dict) else data_model + options = [] + for item in items: + options.append({"id": item[value_column], "name": item[label_column], "data": item}) + return options + @staticmethod def update_task_assignments(processor): """For every upcoming user task, log a task action @@ -519,7 +538,7 @@ class WorkflowService(object): @staticmethod def extract_form_data(latest_data, task): - """Removes data from latest_data that would be added by the child task or any of it's children.""" + """Removes data from latest_data that would be added by the child task or any of its children.""" data = {} if hasattr(task.task_spec, 'form'): diff --git a/crc/static/bpmn/data_security_plan/HIPAA_Ids.xls b/crc/static/bpmn/data_security_plan/HIPAA_Ids.xls new file mode 100644 index 00000000..2d703832 Binary files /dev/null and b/crc/static/bpmn/data_security_plan/HIPAA_Ids.xls differ diff --git a/crc/static/bpmn/data_security_plan/NEW_DSP_template.docx b/crc/static/bpmn/data_security_plan/NEW_DSP_template.docx index 9c282eaa..f6faeb28 100644 Binary files a/crc/static/bpmn/data_security_plan/NEW_DSP_template.docx and b/crc/static/bpmn/data_security_plan/NEW_DSP_template.docx differ diff --git a/crc/static/bpmn/data_security_plan/data_security_plan.bpmn b/crc/static/bpmn/data_security_plan/data_security_plan.bpmn index fc6704fa..0bf95e18 100644 --- a/crc/static/bpmn/data_security_plan/data_security_plan.bpmn +++ b/crc/static/bpmn/data_security_plan/data_security_plan.bpmn @@ -1,5 +1,5 @@ - + SequenceFlow_100w7co @@ -10,45 +10,27 @@ - + + + + - - - - - - - - - - - - - - - - - - - - - + - - + - + @@ -162,28 +144,10 @@ + + + - - - - - - - - - - - - - - - - - - - - - @@ -202,31 +166,14 @@ - + + + + - - - - - - - - - - - - - - - - - - - - @@ -253,9 +200,8 @@ Answer the questions for each of the Individual Use Devices that you use to collect or store your data onto your individual use device during the course of your research. Do not select these items if they are only to be used to connect elsewhere (to the items you identified in Electronic Medical Record, UVA approved eCRF or clinical trials management system, UVA servers & websites, and Web-based server, cloud server, or any non-centrally managed server): - + - @@ -276,95 +222,75 @@ - + - - + - + - - + - + - - + + + + - - - - - - - - - - - - - - - - - - - - - + - - + - + - + - + - + + + + + + + + + + + + + - - + - + - - + - + - - - - - - - - - - + @@ -372,7 +298,7 @@ SequenceFlow_0nc6lcs SequenceFlow_0gp2pjm - + @@ -389,7 +315,7 @@ Indicate all the possible formats in which you will transmit your data outside o - Flow_0cpwkms + SequenceFlow_0gp2pjm SequenceFlow_0mgwas4 @@ -415,7 +341,6 @@ Indicate all the possible formats in which you will transmit your data outside o - @@ -424,48 +349,27 @@ Indicate all the possible formats in which you will transmit your data outside o - + + + - - - - - - - - - - - - - - - - - - - - - - - @@ -498,7 +402,7 @@ Indicate all the possible formats in which you will transmit your data outside o - + @@ -506,7 +410,7 @@ Indicate all the possible formats in which you will transmit your data outside o - + @@ -518,6 +422,7 @@ Indicate all the possible formats in which you will transmit your data outside o SequenceFlow_0lere0k + Done message SequenceFlow_16kyite @@ -568,7 +473,10 @@ Process: The Study Team will answer the questions in this section to create the How to The Data Security Plan is auto-generated based on your answers on this Step. You can save your information here and check the outcomes on the Data Security Plan Upload Step at any time. -Submit the step only when you are ready. After you "Submit" the step, the information will not be available for editing. +Submit the step only when you are ready. After you "Submit" the step, the information will not be available for editing. + + +# test @@ -623,339 +531,222 @@ Indicate all the possible formats in which you will collect or receive your orig SequenceFlow_0blyor8 SequenceFlow_1oq4w2h - - SequenceFlow_0gp2pjm - Flow_0cpwkms - - - - - > Instructions -o Hippa Instructions -o Hippa Indentifiers -o Vuew Definitions and Instructions -o Paper Documents -o Emailed to UVA Personnel -o EMC (EPIC) -o UVA Approvled eCRF -o UVA Servers -o Web or Cloud Server -o Individual Use Devices -o Device Details -0 Outside of UVA - -o Outside of UVA? -     o Yes  -           o Email Methods -           o Data Management -           o Transmission Method -           o Generate DSP  -    o No -           o Generate DSP - - - - *  Instructions -* Hippa Instructions -* Hippa Indentifiers -o Vuew Definitions and Instructions ->> Paper Documents -> Emailed to UVA Personnel -> EMC (EPIC) -> UVA Approvled eCRF -> UVA Servers -> Web or Cloud Server -o Individual Use Devices -o Device Details -o Outside of UVA - -o Outside of UVA? -     o Yes  -           o Email Methods -           o Data Management -           o Transmission Method -           o Generate DSP  -    o No -           o Generate DSP - - - - * Instructions -* Hippa Instructions -* Hippa Indentifiers -* View Definitions and Instructions - - -* Paper Documents (Parallel creates spaces) -* Emailed to UVA Personnel -* EMC (EPIC) -* UVA Approvled eCRF -* UVA Servers -* Web or Cloud Server -* Individual Use Devices - -o Device Details (MultiInstance Indents, Parallel creates spaces)) - > Desktop - >> Laptop - > Cell Phone - > Other - -o Outside of UVA - -o Outside of UVA? -     o Yes  -           o Email Methods -           o Data Management -           o Transmission Method -           o Generate DSP  -    o No -           o Generate DSP - - - - - - - - - - - - - + + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - - - + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - + + - - + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - - - + + + - + - + - + - + - + - + - + + + + + + + - + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn b/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn index ba522b78..99edb961 100644 --- a/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn +++ b/crc/static/bpmn/irb_api_personnel/irb_api_personnel.bpmn @@ -10,27 +10,38 @@ StudyInfo investigators + ## The following information was gathered: +{% for type, investigator in StudyInfo.investigators.items() %} +### {{investigator.label}}: {{investigator.display_name}} + * Edit Acess? {{investigator.edit_access}} + * Send Emails? {{investigator.emails}} +{% if investigator.label == "Primary Investigator" %} + * Experience: {{investigator.experience}} +{% endif %} +{% endfor %} Flow_1mplloa - + ### Please provide supplemental information for: -#### Investigator : {{investigator.display_name}} -##### Role: {{investigator.type_full}} +#### {{investigator.display_name}} ##### Title: {{investigator.title}} ##### Department: {{investigator.department}} ##### Affiliation: {{investigator.affiliation}} - - - + + + - + + + + Flow_1dcsioh Flow_1mplloa @@ -43,28 +54,28 @@ - - + + - - + + - - + + - + - - - - + - + + + + diff --git a/tests/data/decision_table_invalid/bad_dmn.dmn b/tests/data/decision_table_invalid/bad_dmn.dmn new file mode 100644 index 00000000..fc846175 --- /dev/null +++ b/tests/data/decision_table_invalid/bad_dmn.dmn @@ -0,0 +1,50 @@ + + + + + + + + + + 1 + + + + + + + 0 + + + 0 + + + + 'one' can't be evaluated, it must be quoted + + 1 + + + one + + + + + 2 + + + 2 + + + + + > 2 + + + 3 + + + + + diff --git a/tests/data/decision_table_invalid/decision_table_invalid.bpmn b/tests/data/decision_table_invalid/decision_table_invalid.bpmn new file mode 100644 index 00000000..bbf0473a --- /dev/null +++ b/tests/data/decision_table_invalid/decision_table_invalid.bpmn @@ -0,0 +1,56 @@ + + + + + SequenceFlow_1ma1wxb + + + + SequenceFlow_1ma1wxb + SequenceFlow_0grui6f + + + # Great Work! + +Based on the information you provided (Ginger left {{num_presents}}, we recommend the following statement be provided to Ginger: + +## {{message}} + +We hope you both have an excellent day! + SequenceFlow_0grui6f + + + + This DMN isn't provided enough information to execute + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/enum_options_from_task_data/enum_options_from_task_data.bpmn b/tests/data/enum_options_from_task_data/enum_options_from_task_data.bpmn new file mode 100644 index 00000000..5be4401a --- /dev/null +++ b/tests/data/enum_options_from_task_data/enum_options_from_task_data.bpmn @@ -0,0 +1,100 @@ + + + + + SequenceFlow_0lvudp8 + + + + SequenceFlow_02vev7n + + + + + + + + + + + + + + + Flow_1yet4a9 + SequenceFlow_02vev7n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SequenceFlow_0lvudp8 + Flow_1yet4a9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index 702e8a89..8284313d 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -333,6 +333,31 @@ class TestTasksApi(BaseTest): self.assert_options_populated(results, ['CUSTOMER_NUMBER', 'CUSTOMER_NAME', 'CUSTOMER_CLASS_MEANING']) self.assertIsInstance(results[0]['data'], dict) + def test_enum_from_task_data(self): + self.load_example_data() + workflow = self.create_workflow('enum_options_from_task_data') + # get the first form in the two form workflow. + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + workflow_api = self.complete_form(workflow, task, {'invitees': [ + {'first_name': 'Alistair', 'last_name': 'Aardvark', 'age': 43, 'likes_pie': True, 'num_lumps': 21, 'secret_id': 'Antimony', 'display_name': 'Professor Alistair A. Aardvark'}, + {'first_name': 'Berthilda', 'last_name': 'Binturong', 'age': 12, 'likes_pie': False, 'num_lumps': 34, 'secret_id': 'Beryllium', 'display_name': 'Dr. Berthilda B. Binturong'}, + {'first_name': 'Chesterfield', 'last_name': 'Capybara', 'age': 32, 'likes_pie': True, 'num_lumps': 1, 'secret_id': 'Cadmium', 'display_name': 'The Honorable C. C. Capybara'}, + ]}) + task = workflow_api.next_task + + field_id = task.form['fields'][0]['id'] + options = task.form['fields'][0]['options'] + self.assertEqual(3, len(options)) + option_id = options[0]['id'] + self.assertEqual('Professor Alistair A. Aardvark', options[0]['name']) + self.assertEqual('Dr. Berthilda B. Binturong', options[1]['name']) + self.assertEqual('The Honorable C. C. Capybara', options[2]['name']) + self.assertEqual('Alistair', options[0]['data']['first_name']) + self.assertEqual('Berthilda', options[1]['data']['first_name']) + self.assertEqual('Chesterfield', options[2]['data']['first_name']) + def test_lookup_endpoint_for_task_ldap_field_lookup(self): self.load_example_data() workflow = self.create_workflow('ldap_lookup') diff --git a/tests/workflow/test_workflow_service.py b/tests/workflow/test_workflow_service.py index a4c41c7c..748dcedc 100644 --- a/tests/workflow/test_workflow_service.py +++ b/tests/workflow/test_workflow_service.py @@ -9,6 +9,7 @@ from example_data import ExampleDataLoader from crc import db from crc.models.task_event import TaskEventModel from crc.models.api_models import Task +from crc.api.common import ApiError class TestWorkflowService(BaseTest): @@ -130,4 +131,7 @@ class TestWorkflowService(BaseTest): # The first task should be empty, with all the data removed. self.assertEqual({}, task_logs[0].form_data) - + def test_dmn_evaluation_errors_in_oncomplete_raise_api_errors_during_validation(self): + workflow_spec_model = self.load_test_spec("decision_table_invalid") + with self.assertRaises(ApiError): + WorkflowService.test_spec(workflow_spec_model.id) \ No newline at end of file diff --git a/tests/workflow/test_workflow_spec_validation_api.py b/tests/workflow/test_workflow_spec_validation_api.py index cb9b6b77..d79986cf 100644 --- a/tests/workflow/test_workflow_spec_validation_api.py +++ b/tests/workflow/test_workflow_spec_validation_api.py @@ -92,7 +92,7 @@ class TestWorkflowSpecValidation(BaseTest): self.load_example_data() errors = self.validate_workflow("invalid_script") self.assertEqual(2, len(errors)) - self.assertEqual("workflow_validation_exception", errors[0]['code']) + self.assertEqual("error_loading_workflow", errors[0]['code']) self.assertTrue("NoSuchScript" in errors[0]['message']) self.assertEqual("Invalid_Script_Task", errors[0]['task_id']) self.assertEqual("An Invalid Script Reference", errors[0]['task_name'])