Merge branch 'dev' into cr-connect-92-scripting-enhancements

# Conflicts:
#	crc/static/bpmn/ind_supplement/ind_supplement.bpmn
#	tests/workflow/test_workflow_service.py
This commit is contained in:
Dan Funk 2020-07-20 13:01:23 -04:00
commit 6caf44544c
28 changed files with 1487 additions and 545 deletions

View File

@ -15,7 +15,8 @@ TEST_UID = environ.get('TEST_UID', default="dhf8r")
ADMIN_UIDS = re.split(r',\s*', environ.get('ADMIN_UIDS', default="dhf8r,ajl2j,cah3us,cl3wf"))
# Sentry flag
ENABLE_SENTRY = environ.get('ENABLE_SENTRY', default="false") == "true"
ENABLE_SENTRY = environ.get('ENABLE_SENTRY', default="false") == "true" # To be removed soon
SENTRY_ENVIRONMENT = environ.get('SENTRY_ENVIRONMENT', None)
# Add trailing slash to base path
APPLICATION_ROOT = re.sub(r'//', '/', '/%s/' % environ.get('APPLICATION_ROOT', default="/").strip('/'))

View File

@ -52,8 +52,9 @@ origins_re = [r"^https?:\/\/%s(.*)" % o.replace('.', '\.') for o in app.config['
cors = CORS(connexion_app.app, origins=origins_re)
# Sentry error handling
if app.config['ENABLE_SENTRY']:
if app.config['SENTRY_ENVIRONMENT']:
sentry_sdk.init(
environment=app.config['SENTRY_ENVIRONMENT'],
dsn="https://25342ca4e2d443c6a5c49707d68e9f40@o401361.ingest.sentry.io/5260915",
integrations=[FlaskIntegration()]
)

View File

@ -502,7 +502,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/File"
# /v1.0/workflow/0
/reference_file:
get:
operationId: crc.api.file.get_reference_files
@ -565,6 +564,26 @@ paths:
type: string
format: binary
example: '<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions></bpmn:definitions>'
/task_events:
parameters:
- name: action
in: query
required: false
description: The type of action the event documents, options include "ASSIGNMENT" for tasks that are waiting on you, "COMPLETE" for things have completed.
schema:
type: string
get:
operationId: crc.api.workflow.get_task_events
summary: Returns a list of task events related to the current user. Can be filtered by type.
tags:
- Workflows and Tasks
responses:
'200':
description: Returns details about tasks that are waiting on the current user.
content:
application/json:
schema:
$ref: "#/components/schemas/TaskEvent"
# /v1.0/workflow/0
/workflow/{workflow_id}:
parameters:
@ -1192,6 +1211,36 @@ components:
value: "model.my_boolean_field_id && model.my_enum_field_value !== 'something'"
- id: "hide_expression"
value: "model.my_enum_field_value === 'something'"
TaskEvent:
properties:
workflow:
$ref: "#/components/schemas/Workflow"
study:
$ref: "#/components/schemas/Study"
workflow_sec:
$ref: "#/components/schemas/WorkflowSpec"
spec_version:
type: string
action:
type: string
task_id:
type: string
task_type:
type: string
task_lane:
type: string
form_data:
type: object
mi_type:
type: string
mi_count:
type: integer
mi_index:
type: integer
process_name:
type: string
date:
type: string
Form:
properties:
key:

View File

@ -12,7 +12,7 @@ from crc import db, app
from crc.api.user import verify_token, verify_token_admin
from crc.models.approval import ApprovalModel
from crc.models.file import FileModel
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel

View File

@ -25,6 +25,7 @@ class ApiError(Exception):
instance.task_name = task.task_spec.description or ""
instance.file_name = task.workflow.spec.file or ""
instance.task_data = task.data
app.logger.error(message, exc_info=True)
return instance
@classmethod
@ -35,6 +36,7 @@ class ApiError(Exception):
instance.task_name = task_spec.description or ""
if task_spec._wf_spec:
instance.file_name = task_spec._wf_spec.file
app.logger.error(message, exc_info=True)
return instance
@classmethod

View File

@ -6,7 +6,8 @@ from crc import session, app
from crc.api.common import ApiError, ApiErrorSchema
from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema
from crc.models.file import FileModel, LookupDataSchema
from crc.models.stats import TaskEventModel
from crc.models.study import StudyModel, WorkflowMetadata
from crc.models.task_event import TaskEventModel, TaskEventModelSchema, TaskEvent, TaskEventSchema
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \
WorkflowSpecCategoryModelSchema
from crc.services.file_service import FileService
@ -87,7 +88,7 @@ def delete_workflow_specification(spec_id):
session.query(TaskEventModel).filter(TaskEventModel.workflow_spec_id == spec_id).delete()
# Delete all stats and workflow models related to this specification
# Delete all events and workflow models related to this specification
for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id):
StudyService.delete_workflow(workflow)
session.query(WorkflowSpecModel).filter_by(id=spec_id).delete()
@ -98,19 +99,38 @@ def get_workflow(workflow_id, soft_reset=False, hard_reset=False):
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
WorkflowService.update_task_assignments(processor)
return WorkflowApiSchema().dump(workflow_api_model)
def get_task_events(action):
"""Provides a way to see a history of what has happened, or get a list of tasks that need your attention."""
query = session.query(TaskEventModel).filter(TaskEventModel.user_uid == g.user.uid)
if action:
query = query.filter(TaskEventModel.action == action)
events = query.all()
# Turn the database records into something a little richer for the UI to use.
task_events = []
for event in events:
study = session.query(StudyModel).filter(StudyModel.id == event.study_id).first()
workflow = session.query(WorkflowModel).filter(WorkflowModel.id == event.workflow_id).first()
workflow_meta = WorkflowMetadata.from_workflow(workflow)
task_events.append(TaskEvent(event, study, workflow_meta))
return TaskEventSchema(many=True).dump(task_events)
def delete_workflow(workflow_id):
StudyService.delete_workflow(workflow_id)
def set_current_task(workflow_id, task_id):
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
user_uid = __get_user_uid(workflow_model.study.user_uid)
processor = WorkflowProcessor(workflow_model)
task_id = uuid.UUID(task_id)
spiff_task = processor.bpmn_workflow.get_task(task_id)
_verify_user_and_role(processor, spiff_task)
user_uid = g.user.uid
if spiff_task.state != spiff_task.COMPLETED and spiff_task.state != spiff_task.READY:
raise ApiError("invalid_state", "You may not move the token to a task who's state is not "
"currently set to COMPLETE or READY.")
@ -120,41 +140,42 @@ def set_current_task(workflow_id, task_id):
spiff_task.reset_token(reset_data=True) # Don't try to copy the existing data back into this task.
processor.save()
WorkflowService.log_task_action(user_uid, workflow_model, spiff_task,
WorkflowService.TASK_ACTION_TOKEN_RESET,
version=processor.get_version_string())
WorkflowService.log_task_action(user_uid, processor, spiff_task, WorkflowService.TASK_ACTION_TOKEN_RESET)
WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor, spiff_task)
return WorkflowApiSchema().dump(workflow_api_model)
def update_task(workflow_id, task_id, body, terminate_loop=None):
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
if workflow_model is None:
raise ApiError("invalid_workflow_id", "The given workflow id is not valid.", status_code=404)
elif workflow_model.study is None:
raise ApiError("invalid_study", "There is no study associated with the given workflow.", status_code=404)
user_uid = __get_user_uid(workflow_model.study.user_uid)
processor = WorkflowProcessor(workflow_model)
task_id = uuid.UUID(task_id)
spiff_task = processor.bpmn_workflow.get_task(task_id)
_verify_user_and_role(processor, spiff_task)
if not spiff_task:
raise ApiError("empty_task", "Processor failed to obtain task.", status_code=404)
if spiff_task.state != spiff_task.READY:
raise ApiError("invalid_state", "You may not update a task unless it is in the READY state. "
"Consider calling a token reset to make this task Ready.")
if terminate_loop:
spiff_task.terminate_loop()
spiff_task.update_data(body)
processor.complete_task(spiff_task)
processor.do_engine_steps()
processor.save()
WorkflowService.log_task_action(user_uid, workflow_model, spiff_task, WorkflowService.TASK_ACTION_COMPLETE,
version=processor.get_version_string())
# Log the action, and any pending task assignments in the event of lanes in the workflow.
WorkflowService.log_task_action(g.user.uid, processor, spiff_task, WorkflowService.TASK_ACTION_COMPLETE)
WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
return WorkflowApiSchema().dump(workflow_api_model)
@ -210,13 +231,21 @@ def lookup(workflow_id, field_id, query=None, value=None, limit=10):
return LookupDataSchema(many=True).dump(lookup_data)
def __get_user_uid(user_uid):
if 'user' in g:
if g.user.uid not in app.config['ADMIN_UIDS'] and user_uid != g.user.uid:
raise ApiError("permission_denied", "You are not authorized to edit the task data for this workflow.",
status_code=403)
else:
def _verify_user_and_role(processor, spiff_task):
"""Assures the currently logged in user can access the given workflow and task, or
raises an error.
Allow administrators to modify tasks, otherwise assure that the current user
is allowed to edit or update the task. Will raise the appropriate error if user
is not authorized. """
if 'user' not in g:
raise ApiError("logged_out", "You are no longer logged in.", status_code=401)
if g.user.uid in app.config['ADMIN_UIDS']:
return g.user.uid
else:
raise ApiError("logged_out", "You are no longer logged in.", status_code=401)
allowed_users = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if g.user.uid not in allowed_users:
raise ApiError.from_task("permission_denied",
f"This task must be completed by '{allowed_users}', "
f"but you are {g.user.uid}", spiff_task)

View File

@ -29,6 +29,7 @@ class NavigationItem(object):
self.state = state
self.is_decision = is_decision
self.task = task
self.lane = lane
class Task(object):
@ -63,8 +64,9 @@ class Task(object):
##########################################################################
def __init__(self, id, name, title, type, state, form, documentation, data,
multi_instance_type, multi_instance_count, multi_instance_index, process_name, properties):
def __init__(self, id, name, title, type, state, lane, form, documentation, data,
multi_instance_type, multi_instance_count, multi_instance_index,
process_name, properties):
self.id = id
self.name = name
self.title = title
@ -73,6 +75,7 @@ class Task(object):
self.form = form
self.documentation = documentation
self.data = data
self.lane = lane
self.multi_instance_type = multi_instance_type # Some tasks have a repeat behavior.
self.multi_instance_count = multi_instance_count # This is the number of times the task could repeat.
self.multi_instance_index = multi_instance_index # And the index of the currently repeating task.
@ -111,7 +114,7 @@ class FormSchema(ma.Schema):
class TaskSchema(ma.Schema):
class Meta:
fields = ["id", "name", "title", "type", "state", "form", "documentation", "data", "multi_instance_type",
fields = ["id", "name", "title", "type", "state", "lane", "form", "documentation", "data", "multi_instance_type",
"multi_instance_count", "multi_instance_index", "process_name", "properties"]
multi_instance_type = EnumField(MultiInstanceType)
@ -119,6 +122,7 @@ class TaskSchema(ma.Schema):
form = marshmallow.fields.Nested(FormSchema, required=False, allow_none=True)
title = marshmallow.fields.String(required=False, allow_none=True)
process_name = marshmallow.fields.String(required=False, allow_none=True)
lane = marshmallow.fields.String(required=False, allow_none=True)
@marshmallow.post_load
def make_task(self, data, **kwargs):
@ -128,10 +132,11 @@ class TaskSchema(ma.Schema):
class NavigationItemSchema(ma.Schema):
class Meta:
fields = ["id", "task_id", "name", "title", "backtracks", "level", "indent", "child_count", "state",
"is_decision", "task"]
"is_decision", "task", "lane"]
unknown = INCLUDE
task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False, allow_none=True)
backtracks = marshmallow.fields.String(required=False, allow_none=True)
lane = marshmallow.fields.String(required=False, allow_none=True)
title = marshmallow.fields.String(required=False, allow_none=True)
task_id = marshmallow.fields.String(required=False, allow_none=True)

View File

@ -1,33 +0,0 @@
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from crc import db
class TaskEventModel(db.Model):
__tablename__ = 'task_event'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=False)
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=False)
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False)
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
spec_version = db.Column(db.String)
action = db.Column(db.String)
task_id = db.Column(db.String)
task_name = db.Column(db.String)
task_title = db.Column(db.String)
task_type = db.Column(db.String)
task_state = db.Column(db.String)
form_data = db.Column(db.JSON) # And form data submitted when the task was completed.
mi_type = db.Column(db.String)
mi_count = db.Column(db.Integer)
mi_index = db.Column(db.Integer)
process_name = db.Column(db.String)
date = db.Column(db.DateTime)
class TaskEventModelSchema(SQLAlchemyAutoSchema):
class Meta:
model = TaskEventModel
load_instance = True
include_relationships = True
include_fk = True # Includes foreign keys

64
crc/models/task_event.py Normal file
View File

@ -0,0 +1,64 @@
from marshmallow import INCLUDE, fields
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from crc import db, ma
from crc.models.study import StudyModel, StudySchema, WorkflowMetadataSchema, WorkflowMetadata
from crc.models.workflow import WorkflowModel
class TaskEventModel(db.Model):
__tablename__ = 'task_event'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=False)
user_uid = db.Column(db.String, nullable=False) # In some cases the unique user id may not exist in the db yet.
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False)
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
spec_version = db.Column(db.String)
action = db.Column(db.String)
task_id = db.Column(db.String)
task_name = db.Column(db.String)
task_title = db.Column(db.String)
task_type = db.Column(db.String)
task_state = db.Column(db.String)
task_lane = db.Column(db.String)
form_data = db.Column(db.JSON) # And form data submitted when the task was completed.
mi_type = db.Column(db.String)
mi_count = db.Column(db.Integer)
mi_index = db.Column(db.Integer)
process_name = db.Column(db.String)
date = db.Column(db.DateTime)
class TaskEventModelSchema(SQLAlchemyAutoSchema):
class Meta:
model = TaskEventModel
load_instance = True
include_relationships = True
include_fk = True # Includes foreign keys
class TaskEvent(object):
def __init__(self, model: TaskEventModel, study: StudyModel, workflow: WorkflowMetadata):
self.id = model.id
self.study = study
self.workflow = workflow
self.user_uid = model.user_uid
self.action = model.action
self.task_id = model.task_id
self.task_title = model.task_title
self.task_name = model.task_name
self.task_type = model.task_type
self.task_state = model.task_state
self.task_lane = model.task_lane
class TaskEventSchema(ma.Schema):
study = fields.Nested(StudySchema, dump_only=True)
workflow = fields.Nested(WorkflowMetadataSchema, dump_only=True)
class Meta:
model = TaskEvent
additional = ["id", "user_uid", "action", "task_id", "task_title",
"task_name", "task_type", "task_state", "task_lane"]
unknown = INCLUDE

View File

@ -13,7 +13,7 @@ from crc.api.common import ApiError
from crc.models.file import FileModel, FileModelSchema, File
from crc.models.ldap import LdapSchema
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel, Study, Category, WorkflowMetadata
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus

View File

@ -1,6 +1,7 @@
import copy
import json
import string
import uuid
from datetime import datetime
import random
@ -15,13 +16,14 @@ from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.specs import CancelTask, StartTask
from SpiffWorkflow.util.deep_merge import DeepMerge
from flask import g
from jinja2 import Template
from crc import db, app
from crc.api.common import ApiError
from crc.models.api_models import Task, MultiInstanceType, NavigationItem, NavigationItemSchema, WorkflowApi
from crc.models.file import LookupDataModel
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel, WorkflowStatus, WorkflowSpecModel
@ -32,10 +34,13 @@ from crc.services.workflow_processor import WorkflowProcessor
class WorkflowService(object):
TASK_ACTION_COMPLETE = "Complete"
TASK_ACTION_TOKEN_RESET = "Backwards Move"
TASK_ACTION_HARD_RESET = "Restart (Hard)"
TASK_ACTION_SOFT_RESET = "Restart (Soft)"
TASK_ACTION_COMPLETE = "COMPLETE"
TASK_ACTION_TOKEN_RESET = "TOKEN_RESET"
TASK_ACTION_HARD_RESET = "HARD_RESET"
TASK_ACTION_SOFT_RESET = "SOFT_RESET"
TASK_ACTION_ASSIGNMENT = "ASSIGNMENT" # Whenever the lane changes between tasks we assign the task to specifc user.
TASK_STATE_LOCKED = "LOCKED" # When the task belongs to a different user.
"""Provides tools for processing workflows and tasks. This
should at some point, be the only way to work with Workflows, and
@ -94,11 +99,16 @@ class WorkflowService(object):
processor.bpmn_workflow.do_engine_steps()
tasks = processor.bpmn_workflow.get_tasks(SpiffTask.READY)
for task in tasks:
if task.task_spec.lane is not None and task.task_spec.lane not in task.data:
raise ApiError.from_task("invalid_role",
f"This task is in a lane called '{task.task_spec.lane}', The "
f" current task data must have information mapping this role to "
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 documentation, and raise those errors.
WorkflowService.populate_form_with_random_data(task, task_api, required_only)
task.complete()
processor.complete_task(task)
except WorkflowException as we:
WorkflowService.delete_test_data()
raise ApiError.from_workflow_exception("workflow_validation_exception", str(we), we)
@ -133,20 +143,37 @@ class WorkflowService(object):
@staticmethod
def get_random_data_for_field(field, task):
if field.type == "enum":
has_ldap_lookup = field.has_property(Task.PROP_LDAP_LOOKUP)
has_file_lookup = field.has_property(Task.PROP_OPTIONS_FILE_NAME)
has_data_lookup = field.has_property(Task.PROP_OPTIONS_DATA_NAME)
has_lookup = has_ldap_lookup or has_file_lookup or has_data_lookup
if field.type == "enum" and not has_lookup:
# If it's a normal enum field with no lookup,
# return a random option.
if len(field.options) > 0:
random_choice = random.choice(field.options)
if isinstance(random_choice, dict):
return random.choice(field.options)['id']
choice = random.choice(field.options)
return {
'value': choice['id'],
'label': choice['name']
}
else:
# fixme: why it is sometimes an EnumFormFieldOption, and other times not?
return random_choice.id ## Assume it is an EnumFormFieldOption
# Assume it is an EnumFormFieldOption
return {
'value': random_choice.id,
'label': random_choice.name
}
else:
raise ApiError.from_task("invalid_enum", "You specified an enumeration field (%s),"
" with no options" % field.id, task)
elif field.type == "autocomplete":
elif field.type == "autocomplete" or field.type == "enum":
# If it has a lookup, get the lookup model from the spreadsheet or task data, then return a random option
# from the lookup model
lookup_model = LookupService.get_lookup_model(task, field)
if field.has_property(Task.PROP_LDAP_LOOKUP): # All ldap records get the same person.
if has_ldap_lookup: # All ldap records get the same person.
return {
"label": "dhf8r",
"value": "Dan Funk",
@ -162,9 +189,7 @@ class WorkflowService(object):
elif lookup_model:
data = db.session.query(LookupDataModel).filter(
LookupDataModel.lookup_file_model == lookup_model).limit(10).all()
options = []
for d in data:
options.append({"id": d.value, "label": d.label})
options = [{"value": d.value, "label": d.label, "data": d.data} for d in data]
return random.choice(options)
else:
raise ApiError.from_task("unknown_lookup_option", "The settings for this auto complete field "
@ -197,13 +222,15 @@ class WorkflowService(object):
possible, next_task is set to the current_task."""
nav_dict = processor.bpmn_workflow.get_nav_list()
# Some basic cleanup of the title for the for the navigation.
navigation = []
for nav_item in nav_dict:
spiff_task = processor.bpmn_workflow.get_task(nav_item['task_id'])
if 'description' in nav_item:
nav_item['title'] = nav_item.pop('description')
# fixme: duplicate code from the workflow_service. Should only do this in one place.
if ' ' in nav_item['title']:
if nav_item['title'] is not None and ' ' in nav_item['title']:
nav_item['title'] = nav_item['title'].partition(' ')[2]
else:
nav_item['title'] = ""
@ -211,11 +238,13 @@ class WorkflowService(object):
nav_item['task'] = WorkflowService.spiff_task_to_api_task(spiff_task, add_docs_and_forms=False)
nav_item['title'] = nav_item['task'].title # Prefer the task title.
user_uids = WorkflowService.get_users_assigned_to_task(processor, spiff_task)
if 'user' not in g or not g.user or g.user.uid not in user_uids:
nav_item['state'] = WorkflowService.TASK_STATE_LOCKED
else:
nav_item['task'] = None
if not 'is_decision' in nav_item:
nav_item['is_decision'] = False
navigation.append(NavigationItem(**nav_item))
NavigationItemSchema().dump(nav_item)
@ -241,7 +270,10 @@ class WorkflowService(object):
previous_form_data = WorkflowService.get_previously_submitted_data(processor.workflow_model.id, next_task)
DeepMerge.merge(next_task.data, previous_form_data)
workflow_api.next_task = WorkflowService.spiff_task_to_api_task(next_task, add_docs_and_forms=True)
# Update the state of the task to locked if the current user does not own the task.
user_uids = WorkflowService.get_users_assigned_to_task(processor, next_task)
if 'user' not in g or not g.user or g.user.uid not in user_uids:
workflow_api.next_task.state = WorkflowService.TASK_STATE_LOCKED
return workflow_api
@staticmethod
@ -299,11 +331,17 @@ class WorkflowService(object):
for key, val in spiff_task.task_spec.extensions.items():
props[key] = val
if hasattr(spiff_task.task_spec, 'lane'):
lane = spiff_task.task_spec.lane
else:
lane = None
task = Task(spiff_task.id,
spiff_task.task_spec.name,
spiff_task.task_spec.description,
task_type,
spiff_task.get_state_name(),
lane,
None,
"",
{},
@ -338,7 +376,7 @@ class WorkflowService(object):
try:
task.title = spiff_task.workflow.script_engine.evaluate_expression(spiff_task, task.properties['display_name'])
except Exception as e:
app.logger.info("Failed to set title on task due to type error." + str(e))
app.logger.error("Failed to set title on task due to type error." + str(e), exc_info=True)
elif task.title and ' ' in task.title:
task.title = task.title.partition(' ')[2]
return task
@ -424,21 +462,50 @@ class WorkflowService(object):
return options
@staticmethod
def log_task_action(user_uid, workflow_model, spiff_task, action, version):
def update_task_assignments(processor):
"""For every upcoming user task, log a task action
that connects the assigned user(s) to that task. All
existing assignment actions for this workflow are removed from the database,
so that only the current valid actions are available. update_task_assignments
should be called whenever progress is made on a workflow."""
db.session.query(TaskEventModel). \
filter(TaskEventModel.workflow_id == processor.workflow_model.id). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).delete()
for task in processor.get_current_user_tasks():
user_ids = WorkflowService.get_users_assigned_to_task(processor, task)
for user_id in user_ids:
WorkflowService.log_task_action(user_id, processor, task, WorkflowService.TASK_ACTION_ASSIGNMENT)
@staticmethod
def get_users_assigned_to_task(processor, spiff_task):
if not hasattr(spiff_task.task_spec, 'lane') or spiff_task.task_spec.lane is None:
return [processor.workflow_model.study.user_uid]
# todo: return a list of all users that can edit the study by default
if spiff_task.task_spec.lane not in spiff_task.data:
return [] # No users are assignable to the task at this moment
lane_users = spiff_task.data[spiff_task.task_spec.lane]
if not isinstance(lane_users, list):
lane_users = [lane_users]
return lane_users
@staticmethod
def log_task_action(user_uid, processor, spiff_task, action):
task = WorkflowService.spiff_task_to_api_task(spiff_task)
form_data = WorkflowService.extract_form_data(spiff_task.data, spiff_task)
task_event = TaskEventModel(
study_id=workflow_model.study_id,
study_id=processor.workflow_model.study_id,
user_uid=user_uid,
workflow_id=workflow_model.id,
workflow_spec_id=workflow_model.workflow_spec_id,
spec_version=version,
workflow_id=processor.workflow_model.id,
workflow_spec_id=processor.workflow_model.workflow_spec_id,
spec_version=processor.get_version_string(),
action=action,
task_id=task.id,
task_name=task.name,
task_title=task.title,
task_type=str(task.type),
task_state=task.state,
task_lane=task.lane,
form_data=form_data,
mi_type=task.multi_instance_type.value, # Some tasks have a repeat behavior.
mi_count=task.multi_instance_count, # This is the number of times the task could repeat.

View File

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_0o0ff2r" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<decision id="decision_ind_check" name="IND Check">
<decisionTable id="decisionTable_1">
<input id="input_1" label="IS_IND">
<inputExpression id="inputExpression_1" typeRef="integer">
<text>StudyInfo.details.IS_IND</text>
</inputExpression>
</input>
<input id="InputClause_1yk6kx1" label="IND_1 Number?">
<inputExpression id="LiteralExpression_00xhtjw" typeRef="string">
<text>StudyInfo.details.IND_1</text>
</inputExpression>
</input>
<input id="InputClause_069sith" label="IND_2 Number?">
<inputExpression id="LiteralExpression_1h9kd8o" typeRef="string">
<text>StudyInfo.details.IND_2</text>
</inputExpression>
</input>
<input id="InputClause_0d0vpur" label="IND_3 Number?">
<inputExpression id="LiteralExpression_0zbsg01" typeRef="string">
<text>StudyInfo.details.IND_3</text>
</inputExpression>
</input>
<output id="output_1" label="Add Supplemental Data" name="ind_supplement" typeRef="boolean" />
<rule id="DecisionRule_0h0od2e">
<inputEntry id="UnaryTests_09ctq71">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1cub5pk">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0aubvru">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0rjeqez">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1we3duh">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_199dgpt">
<inputEntry id="UnaryTests_1ec0msc">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0h3sj7g">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_1ji4kgh">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_10gxrx9">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1fhlpya">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0teanii">
<inputEntry id="UnaryTests_0akfjdp">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1c88e2t">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0zfrdlt">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_07drghr">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_1i7dtia">
<text>true</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0m9aydp">
<inputEntry id="UnaryTests_003n37j">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_1fcaod2">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0hmnsvb">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0y6xian">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1wuhxz7">
<text>false</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -1,127 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1e7871f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_04jm0bm" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1dhb8f4</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1dhb8f4" sourceRef="StartEvent_1" targetRef="ScriptTask_1fn00ox" />
<bpmn:endEvent id="EndEvent_1h89sl4">
<bpmn:incoming>SequenceFlow_1yhv1qz</bpmn:incoming>
<bpmn:incoming>SequenceFlow_1enco3g</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="ScriptTask_1fn00ox" name="Load IRB Details">
<bpmn:incoming>SequenceFlow_1dhb8f4</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1uzcl1f</bpmn:outgoing>
<bpmn:script>#! StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1uzcl1f" sourceRef="ScriptTask_1fn00ox" targetRef="Task_SupplementIDE" />
<bpmn:exclusiveGateway id="ExclusiveGateway_1fib89p">
<bpmn:incoming>SequenceFlow_1lazou8</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1yb1vma</bpmn:outgoing>
<bpmn:outgoing>SequenceFlow_011l5xt</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="SequenceFlow_1yb1vma" name="Yes" sourceRef="ExclusiveGateway_1fib89p" targetRef="UserTask_0a2dfa8">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">ind_supplement == True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="SequenceFlow_011l5xt" name="No" sourceRef="ExclusiveGateway_1fib89p" targetRef="Task_NoIDE">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">ind_supplement == False</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:manualTask id="Task_NoIDE" name="IND But No Numbers">
<bpmn:documentation>The use of an Investigational New Drug (IND) was indicated in Protocol Builder, but no IND number was entered. Please enter up to three numbers in the Supplemental section of Protocol Builder so supplemental information can be entered here.</bpmn:documentation>
<bpmn:incoming>SequenceFlow_011l5xt</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1yhv1qz</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="SequenceFlow_1lazou8" sourceRef="Task_SupplementIDE" targetRef="ExclusiveGateway_1fib89p" />
<bpmn:businessRuleTask id="Task_SupplementIDE" name="Supplement IND?" camunda:decisionRef="decision_ind_check">
<bpmn:incoming>SequenceFlow_1uzcl1f</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1lazou8</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:sequenceFlow id="SequenceFlow_1yhv1qz" sourceRef="Task_NoIDE" targetRef="EndEvent_1h89sl4" />
<bpmn:userTask id="UserTask_0a2dfa8" name="Edit IND Info" camunda:formKey="FormKey_Details">
<bpmn:documentation>IND No.: {{StudyInfo.details.IND_1}}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="HolderType" label="IND Holder Type" type="enum">
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="HolderNameNotInList" label="IND Holder Name if not in above list" type="string" />
<camunda:formField id="DrugBiologicName" label="Drug/Biologic Name" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1yb1vma</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1enco3g</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1enco3g" sourceRef="UserTask_0a2dfa8" targetRef="EndEvent_1h89sl4" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_04jm0bm">
<bpmndi:BPMNEdge id="SequenceFlow_1enco3g_di" bpmnElement="SequenceFlow_1enco3g">
<di:waypoint x="810" y="117" />
<di:waypoint x="932" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1yhv1qz_di" bpmnElement="SequenceFlow_1yhv1qz">
<di:waypoint x="810" y="230" />
<di:waypoint x="871" y="230" />
<di:waypoint x="871" y="117" />
<di:waypoint x="932" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1lazou8_di" bpmnElement="SequenceFlow_1lazou8">
<di:waypoint x="510" y="117" />
<di:waypoint x="585" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_011l5xt_di" bpmnElement="SequenceFlow_011l5xt">
<di:waypoint x="610" y="142" />
<di:waypoint x="610" y="230" />
<di:waypoint x="710" y="230" />
<bpmndi:BPMNLabel>
<dc:Bounds x="618" y="183" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1yb1vma_di" bpmnElement="SequenceFlow_1yb1vma">
<di:waypoint x="635" y="117" />
<di:waypoint x="710" y="117" />
<bpmndi:BPMNLabel>
<dc:Bounds x="659" y="99" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1uzcl1f_di" bpmnElement="SequenceFlow_1uzcl1f">
<di:waypoint x="360" y="117" />
<di:waypoint x="410" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1dhb8f4_di" bpmnElement="SequenceFlow_1dhb8f4">
<di:waypoint x="188" y="117" />
<di:waypoint x="260" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1h89sl4_di" bpmnElement="EndEvent_1h89sl4">
<dc:Bounds x="932" y="99" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="414" y="202" width="74" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1fn00ox_di" bpmnElement="ScriptTask_1fn00ox">
<dc:Bounds x="260" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ExclusiveGateway_1fib89p_di" bpmnElement="ExclusiveGateway_1fib89p" isMarkerVisible="true">
<dc:Bounds x="585" y="92" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ManualTask_1f7z9wm_di" bpmnElement="Task_NoIDE">
<dc:Bounds x="710" y="190" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_1cszgkx_di" bpmnElement="Task_SupplementIDE">
<dc:Bounds x="410" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0a2dfa8_di" bpmnElement="UserTask_0a2dfa8">
<dc:Bounds x="710" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

Binary file not shown.

View File

@ -0,0 +1,220 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_0o0ff2r" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<decision id="decision_ind_check" name="IND Check">
<decisionTable id="decisionTable_1">
<input id="input_1" label="IS_IND">
<inputExpression id="inputExpression_1" typeRef="integer">
<text>StudyInfo.details.IS_IND</text>
</inputExpression>
</input>
<input id="InputClause_1yk6kx1" label="IND_1 Number?">
<inputExpression id="LiteralExpression_00xhtjw" typeRef="string">
<text>StudyInfo.details.IND_1</text>
</inputExpression>
</input>
<input id="InputClause_069sith" label="IND_2 Number?">
<inputExpression id="LiteralExpression_1h9kd8o" typeRef="string">
<text>StudyInfo.details.IND_2</text>
</inputExpression>
</input>
<input id="InputClause_0d0vpur" label="IND_3 Number?">
<inputExpression id="LiteralExpression_0zbsg01" typeRef="string">
<text>StudyInfo.details.IND_3</text>
</inputExpression>
</input>
<output id="output_1" label="Add Supplemental Data" name="ind_supplement" typeRef="boolean" />
<output id="OutputClause_0cfn42v" label="IND Count Entered" name="ind_cnt" typeRef="string" />
<output id="OutputClause_0xcdkqm" label="IND Message" name="ind_message" typeRef="string" />
<output id="OutputClause_08qk83g" label="IND 1 Field Value" name="IND1_Number" typeRef="string" />
<rule id="DecisionRule_0teanii">
<description>3 IND #s</description>
<inputEntry id="UnaryTests_0akfjdp">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1c88e2t">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0zfrdlt">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_07drghr">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_1i7dtia">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_0kulwlr">
<text>"three"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1tw8tzn">
<text>"Three IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1fiijih">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_199dgpt">
<description>2 IND #s</description>
<inputEntry id="UnaryTests_1ec0msc">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0h3sj7g">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_1ji4kgh">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_10gxrx9">
<text>""</text>
</inputEntry>
<outputEntry id="LiteralExpression_1fhlpya">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_1h5mox1">
<text>"two"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1nvcjhv">
<text>"Two IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1rwd1ja">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_0z0tcm0">
<description>3 IND#s, missing #2</description>
<inputEntry id="UnaryTests_1kf86r3">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0jm1wzq">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_14itgac">
<text>""</text>
</inputEntry>
<inputEntry id="UnaryTests_1prht5p">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_0pooubu">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_0nioovi">
<text>"two"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1fa5e2o">
<text>"Two IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_1qul3vr">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_0bwkqh7">
<description>3 IND#s, missing #1</description>
<inputEntry id="UnaryTests_13ig4fh">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_11kb6cw">
<text>""</text>
</inputEntry>
<inputEntry id="UnaryTests_0sfwtwo">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0xxmh5j">
<text>not('')</text>
</inputEntry>
<outputEntry id="LiteralExpression_14otjle">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_13qodmm">
<text>"two"</text>
</outputEntry>
<outputEntry id="LiteralExpression_0xhjgjn">
<text>"Two IND #s entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_13g0u0n">
<text></text>
</outputEntry>
</rule>
<rule id="DecisionRule_0h0od2e">
<description>1 IND #</description>
<inputEntry id="UnaryTests_09ctq71">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_1cub5pk">
<text>not('')</text>
</inputEntry>
<inputEntry id="UnaryTests_0aubvru">
<text>""</text>
</inputEntry>
<inputEntry id="UnaryTests_0rjeqez">
<text>""</text>
</inputEntry>
<outputEntry id="LiteralExpression_1we3duh">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_1jv0san">
<text>"one"</text>
</outputEntry>
<outputEntry id="LiteralExpression_19cvvhd">
<text>"One IND # entered"</text>
</outputEntry>
<outputEntry id="LiteralExpression_15ikz7u">
<text>StudyInfo.details.IND_1</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1nitohs">
<description>No</description>
<inputEntry id="UnaryTests_19oot48">
<text>1</text>
</inputEntry>
<inputEntry id="UnaryTests_0i2qyga">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_09wye05">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_1g4y2ti">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_0c2mi3l">
<text>true</text>
</outputEntry>
<outputEntry id="LiteralExpression_1e2kzvw">
<text>"na"</text>
</outputEntry>
<outputEntry id="LiteralExpression_0wj4zzb">
<text>"No IND Numbers Entered in PB"</text>
</outputEntry>
<outputEntry id="LiteralExpression_049iioi">
<text>""</text>
</outputEntry>
</rule>
<rule id="DecisionRule_0m9aydp">
<description>No IND, PB Q#56 answered as No, should not be needed, but here as stopgap in case memu check failed</description>
<inputEntry id="UnaryTests_003n37j">
<text>0</text>
</inputEntry>
<inputEntry id="UnaryTests_1fcaod2">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0hmnsvb">
<text></text>
</inputEntry>
<inputEntry id="UnaryTests_0y6xian">
<text></text>
</inputEntry>
<outputEntry id="LiteralExpression_1wuhxz7">
<text>false</text>
</outputEntry>
<outputEntry id="LiteralExpression_1dznftw">
<text></text>
</outputEntry>
<outputEntry id="LiteralExpression_1lbt5oy">
<text></text>
</outputEntry>
<outputEntry id="LiteralExpression_0tkt63s">
<text></text>
</outputEntry>
</rule>
</decisionTable>
</decision>
</definitions>

View File

@ -0,0 +1,276 @@
<?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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1e7871f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.0.0">
<bpmn:process id="Process_04jm0bm" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1dhb8f4</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1dhb8f4" sourceRef="StartEvent_1" targetRef="ScriptTask_LoadIRBDetails" />
<bpmn:endEvent id="EndEvent_1h89sl4">
<bpmn:incoming>Flow_0jqdolk</bpmn:incoming>
<bpmn:incoming>Flow_OneOnly</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="ScriptTask_LoadIRBDetails" name="Load IRB Details">
<bpmn:incoming>SequenceFlow_1dhb8f4</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1uzcl1f</bpmn:outgoing>
<bpmn:script>#! StudyInfo details</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1uzcl1f" sourceRef="ScriptTask_LoadIRBDetails" targetRef="Task_SupplementIDE" />
<bpmn:businessRuleTask id="Task_SupplementIDE" name="Current IND Status" camunda:decisionRef="decision_ind_check">
<bpmn:incoming>SequenceFlow_1uzcl1f</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1cwibmt</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:userTask id="IND_n1_info" name="Edit IND #1 Info" camunda:formKey="IND1_Info">
<bpmn:documentation>IND No.: {{ StudyInfo.details.IND_1 }}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND1_Number" label="IND1 number:" type="string">
<camunda:properties>
<camunda:property id="description" value="Enter IND number, if available." />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND1_HolderType" label="IND Holder Type" type="enum">
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="IND1_HolderName" label="Holder Name" type="autocomplete">
<camunda:properties>
<camunda:property id="spreadsheet.name" value="SponsorList.xls" />
<camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND1_HolderNameNotInList" label="IND Holder Name if not in above list" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="model.IND1_HolderName &#38;&#38; model.IND1_HolderName.value !== &#34;0&#34;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND1_DrugBiologicName" label="Drug/Biologic Name" type="string" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1bn0jp7</bpmn:incoming>
<bpmn:outgoing>Flow_10rb7gb</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="IND_n2_info" name="Edit IND #2 Info" camunda:formKey="IND2_Info">
<bpmn:documentation>IND No.:</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND2_Status" label="Do you have a second Investigational New Drug?" type="enum">
<camunda:value id="Yes" name="Yes" />
<camunda:value id="YesBut" name="Yes, but number is not available at this time" />
<camunda:value id="No" name="No" />
</camunda:formField>
<camunda:formField id="IND2_Number" label="IND2 Number:" type="string">
<camunda:properties>
<camunda:property id="value_expression" value="model.StudyInfo.details.IND_2" />
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND2_HolderType" label="IND Holder Type" type="enum">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="IND2_HolderName" label="Holder Name" type="autocomplete">
<camunda:properties>
<camunda:property id="spreadsheet.name" value="SponsorList.xls" />
<camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND2_HolderNameNotInList" label="IND Holder Name if not in above list" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39; || model.IND2_HolderName.value !== &#34;0&#34;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND2_DrugBiologicName" label="Drug/Biologic Name" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND2_Status || !model.IND2_Status.value || model.IND2_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_TwoOrThree</bpmn:incoming>
<bpmn:outgoing>Flow_1p563xr</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="IND_n3_info" name="Edit IND #3 Info" camunda:formKey="IND3_Info">
<bpmn:documentation>IND No.:</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND3_Status" label="Do you have a third Investigational New Drug?" type="enum" defaultValue="No">
<camunda:value id="Yes" name="Yes" />
<camunda:value id="YesBut" name="Yes, but number is not available at this time." />
<camunda:value id="No" name="No" />
</camunda:formField>
<camunda:formField id="IND3_Number" label="IND3 Number:" type="string">
<camunda:properties>
<camunda:property id="value_expression" value="model.StudyInfo.details.IND_3" />
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND3_HolderType" label="IND Holder Type" type="enum">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
<camunda:value id="Industry" name="Industry" />
<camunda:value id="UVaPI" name="UVa PI" />
<camunda:value id="OtherPI" name="Other PI" />
<camunda:value id="UVaCenter" name="UVaCenter" />
<camunda:value id="OtherCollUniv" name="Other Colleges and Universities" />
<camunda:value id="Exempt" name="IND Exempt" />
<camunda:value id="NA" name="NA" />
</camunda:formField>
<camunda:formField id="IND3_HolderName" label="Holder Name" type="autocomplete">
<camunda:properties>
<camunda:property id="spreadsheet.name" value="SponsorList.xls" />
<camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND3_HolderNameNotInList" label="IND Holder Name if not in above list" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39; || model.IND3_HolderName.value !== &#34;0&#34;" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="IND3_DrugBiologicName" label="Drug/Biologic Name" type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.IND3_Status || !model.IND3_Status.value || model.IND3_Status.value === &#39;No&#39;" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1p563xr</bpmn:incoming>
<bpmn:outgoing>Flow_0jqdolk</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_1cwibmt" sourceRef="Task_SupplementIDE" targetRef="Activity_0yf2ypo" />
<bpmn:userTask id="Activity_0yf2ypo" name="Provide IND Count" camunda:formKey="IND_Count">
<bpmn:documentation>{{ ind_message }}</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="IND_CntEntered" label="How Many?" type="enum" defaultValue="one">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="value_one" name="1 IND number" />
<camunda:value id="value_two" name="2 IND number" />
<camunda:value id="value_three" name="3 IND number" />
<camunda:value id="value_na" name="No IND Numbers in PB" />
</camunda:formField>
<camunda:formField id="FormField_0h8vmid" label="Test" type="string">
<camunda:properties>
<camunda:property id="value_expression" value="model.ind_cnt" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_1cwibmt</bpmn:incoming>
<bpmn:outgoing>Flow_1bn0jp7</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1bn0jp7" sourceRef="Activity_0yf2ypo" targetRef="IND_n1_info" />
<bpmn:sequenceFlow id="Flow_1p563xr" sourceRef="IND_n2_info" targetRef="IND_n3_info" />
<bpmn:sequenceFlow id="Flow_0jqdolk" sourceRef="IND_n3_info" targetRef="EndEvent_1h89sl4" />
<bpmn:sequenceFlow id="Flow_10rb7gb" sourceRef="IND_n1_info" targetRef="Gateway_0ckycp9" />
<bpmn:exclusiveGateway id="Gateway_0ckycp9">
<bpmn:incoming>Flow_10rb7gb</bpmn:incoming>
<bpmn:outgoing>Flow_TwoOrThree</bpmn:outgoing>
<bpmn:outgoing>Flow_OneOnly</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_TwoOrThree" name="Two or Three INDs" sourceRef="Gateway_0ckycp9" targetRef="IND_n2_info">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">IND_CntEntered != "value_one"</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_OneOnly" name="One IND" sourceRef="Gateway_0ckycp9" targetRef="EndEvent_1h89sl4">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">IND_CntEntered == "value_one"</bpmn:conditionExpression>
</bpmn:sequenceFlow>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_04jm0bm">
<bpmndi:BPMNEdge id="Flow_00n2n7p_di" bpmnElement="Flow_OneOnly">
<di:waypoint x="940" y="142" />
<di:waypoint x="940" y="260" />
<di:waypoint x="1510" y="260" />
<di:waypoint x="1510" y="135" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1205" y="242" width="43" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1o2u7k3_di" bpmnElement="Flow_TwoOrThree">
<di:waypoint x="965" y="117" />
<di:waypoint x="1070" y="117" />
<bpmndi:BPMNLabel>
<dc:Bounds x="987" y="86" width="65" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_10rb7gb_di" bpmnElement="Flow_10rb7gb">
<di:waypoint x="860" y="117" />
<di:waypoint x="915" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0jqdolk_di" bpmnElement="Flow_0jqdolk">
<di:waypoint x="1380" y="117" />
<di:waypoint x="1492" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1p563xr_di" bpmnElement="Flow_1p563xr">
<di:waypoint x="1170" y="117" />
<di:waypoint x="1280" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1bn0jp7_di" bpmnElement="Flow_1bn0jp7">
<di:waypoint x="670" y="117" />
<di:waypoint x="760" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1cwibmt_di" bpmnElement="SequenceFlow_1cwibmt">
<di:waypoint x="520" y="117" />
<di:waypoint x="570" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1uzcl1f_di" bpmnElement="SequenceFlow_1uzcl1f">
<di:waypoint x="340" y="117" />
<di:waypoint x="420" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1dhb8f4_di" bpmnElement="SequenceFlow_1dhb8f4">
<di:waypoint x="188" y="117" />
<di:waypoint x="240" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1h89sl4_di" bpmnElement="EndEvent_1h89sl4">
<dc:Bounds x="1492" y="99" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="414" y="202" width="74" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1fn00ox_di" bpmnElement="ScriptTask_LoadIRBDetails">
<dc:Bounds x="240" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BusinessRuleTask_1cszgkx_di" bpmnElement="Task_SupplementIDE">
<dc:Bounds x="420" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0a2dfa8_di" bpmnElement="IND_n1_info">
<dc:Bounds x="760" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1smni98_di" bpmnElement="IND_n2_info">
<dc:Bounds x="1070" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1378hd8_di" bpmnElement="IND_n3_info">
<dc:Bounds x="1280" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0wfey2b_di" bpmnElement="Activity_0yf2ypo">
<dc:Bounds x="570" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0ckycp9_di" bpmnElement="Gateway_0ckycp9" isMarkerVisible="true">
<dc:Bounds x="915" y="92" width="50" height="50" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -93,8 +93,8 @@ class ExampleDataLoader:
description="Supplemental information for the IDE number entered in Protocol Builder",
category_id=0,
display_order=3)
self.create_spec(id="ind_supplement",
name="ind_supplement",
self.create_spec(id="ind_update",
name="ind_update",
display_name="IND Supplement Info",
description="Supplement information for the Investigational New Drug(s) specified in Protocol Builder",
category_id=0,

View File

@ -0,0 +1,38 @@
"""empty message
Revision ID: ffef4661a37d
Revises: 5acd138e969c
Create Date: 2020-07-14 19:52:05.270939
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ffef4661a37d'
down_revision = '5acd138e969c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('task_event', sa.Column('task_lane', sa.String(), nullable=True))
op.drop_constraint('task_event_user_uid_fkey', 'task_event', type_='foreignkey')
op.execute("update task_event set action = 'COMPLETE' where action='Complete'")
op.execute("update task_event set action = 'TOKEN_RESET' where action='Backwards Move'")
op.execute("update task_event set action = 'HARD_RESET' where action='Restart (Hard)'")
op.execute("update task_event set action = 'SOFT_RESET' where action='Restart (Soft)'")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key('task_event_user_uid_fkey', 'task_event', 'user', ['user_uid'], ['uid'])
op.drop_column('task_event', 'task_lane')
op.execute("update task_event set action = 'Complete' where action='COMPLETE'")
op.execute("update task_event set action = 'Backwards Move' where action='TOKEN_RESET'")
op.execute("update task_event set action = 'Restart (Hard)' where action='HARD_RESET'")
op.execute("update task_event set action = 'Restart (Soft)' where action='SOFT_RESET'")
# ### end Alembic commands ###

View File

@ -16,7 +16,7 @@ from crc.models.api_models import WorkflowApiSchema, MultiInstanceType
from crc.models.approval import ApprovalModel, ApprovalStatus
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel
@ -230,7 +230,7 @@ class BaseTest(unittest.TestCase):
db.session.commit()
return user
def create_study(self, uid="dhf8r", title="Beer conception in the bipedal software engineer", primary_investigator_id="lb3dp"):
def create_study(self, uid="dhf8r", title="Beer consumption in the bipedal software engineer", primary_investigator_id="lb3dp"):
study = session.query(StudyModel).filter_by(user_uid=uid).filter_by(title=title).first()
if study is None:
user = self.create_user(uid=uid)
@ -263,13 +263,13 @@ class BaseTest(unittest.TestCase):
return full_study
def create_workflow(self, workflow_name, study=None, category_id=None):
def create_workflow(self, workflow_name, study=None, category_id=None, as_user="dhf8r"):
db.session.flush()
spec = db.session.query(WorkflowSpecModel).filter(WorkflowSpecModel.name == workflow_name).first()
if spec is None:
spec = self.load_test_spec(workflow_name, category_id=category_id)
if study is None:
study = self.create_study()
study = self.create_study(uid=as_user)
workflow_model = StudyService._create_workflow_model(study, spec)
return workflow_model
@ -313,6 +313,7 @@ class BaseTest(unittest.TestCase):
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
return workflow_api
def complete_form(self, workflow_in, task_in, dict_data, error_code=None, terminate_loop=None, user_uid="dhf8r"):
prev_completed_task_count = workflow_in.completed_tasks
if isinstance(task_in, dict):
@ -339,7 +340,7 @@ class BaseTest(unittest.TestCase):
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
# Assure stats are updated on the model
# Assure task events are updated on the model
workflow = WorkflowApiSchema().load(json_data)
# The total number of tasks may change over time, as users move through gateways
# branches may be pruned. As we hit parallel Multi-Instance new tasks may be created...
@ -352,6 +353,7 @@ class BaseTest(unittest.TestCase):
task_events = session.query(TaskEventModel) \
.filter_by(workflow_id=workflow.id) \
.filter_by(task_id=task_id) \
.filter_by(action=WorkflowService.TASK_ACTION_COMPLETE) \
.order_by(TaskEventModel.date.desc()).all()
self.assertGreater(len(task_events), 0)
event = task_events[0]

View File

@ -0,0 +1,177 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0ybr9ph" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:collaboration id="Collaboration_0xjb3la">
<bpmn:participant id="Participant_0ozb2sp" processRef="Process_1aebbrh" />
</bpmn:collaboration>
<bpmn:process id="Process_1aebbrh" isExecutable="true">
<bpmn:laneSet id="LaneSet_0ilprw6">
<bpmn:lane id="Lane_1s1s7a1">
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_1hljoeq</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Event_0lscajc</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_19ccxoj</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="Lane_1m47545" name="supervisor">
<bpmn:flowNodeRef>Gateway_1fkgc4u</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_14eor1x</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0a7090c</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Activity_1hljoeq" name="Request Approval" camunda:formKey="form">
<bpmn:documentation># Answer me these questions 3, ere the other side you see!</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="favorite_color" label="What is your favorite color?" type="string" defaultValue="Yellow" />
<camunda:formField id="quest" label="What is your quest?" type="string" defaultValue="To seek the holly Grail!" />
<camunda:formField id="swallow_speed" label="What is the air speed velocity of an unladen swallow?" defaultValue="About 24 miles per hour" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0a7090c</bpmn:incoming>
<bpmn:incoming>Flow_070gq5r</bpmn:incoming>
<bpmn:outgoing>Flow_1hcpt7c</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_1fkgc4u">
<bpmn:incoming>Flow_1gp4zfd</bpmn:incoming>
<bpmn:outgoing>Flow_0vnghsi</bpmn:outgoing>
<bpmn:outgoing>Flow_1g38q6b</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="Event_0lscajc">
<bpmn:documentation># Your responses were approved!
Gosh! you must really know a lot about colors and swallows and stuff!
Your supervisor provided the following feedback:
{{feedback}}
You are all done! WARNING: If you go back and reanswer the questions it will create a new approval request.
</bpmn:documentation>
<bpmn:incoming>Flow_1g38q6b</bpmn:incoming>
</bpmn:endEvent>
<bpmn:manualTask id="Activity_19ccxoj" name="Review Feedback">
<bpmn:documentation># Your Request was rejected
Perhaps you don't know the right answer to one of the questions.
Your Supervisor provided the following feedback:
{{feedback}}
Please press save to re-try the questions, and submit your responses again.
</bpmn:documentation>
<bpmn:incoming>Flow_0vnghsi</bpmn:incoming>
<bpmn:outgoing>Flow_070gq5r</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:userTask id="Activity_14eor1x" name="Approve Responses" camunda:formKey="form2">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="approval" label="I approve of this information" type="boolean" defaultValue="false" />
<camunda:formField id="feedback" label="Feedback" type="string" defaultValue="Please provide any feedback you have here." />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1hcpt7c</bpmn:incoming>
<bpmn:outgoing>Flow_1gp4zfd</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0a7090c" sourceRef="StartEvent_1" targetRef="Activity_1hljoeq" />
<bpmn:sequenceFlow id="Flow_1gp4zfd" sourceRef="Activity_14eor1x" targetRef="Gateway_1fkgc4u" />
<bpmn:sequenceFlow id="Flow_0vnghsi" name="rejected" sourceRef="Gateway_1fkgc4u" targetRef="Activity_19ccxoj">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1g38q6b" name="approved" sourceRef="Gateway_1fkgc4u" targetRef="Event_0lscajc">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1hcpt7c" sourceRef="Activity_1hljoeq" targetRef="Activity_14eor1x" />
<bpmn:sequenceFlow id="Flow_070gq5r" sourceRef="Activity_19ccxoj" targetRef="Activity_1hljoeq" />
<bpmn:textAnnotation id="TextAnnotation_1ys83yq">
<bpmn:text>Removed a field that would set the supervisor, making this not validate.</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1kcb9ou" sourceRef="Activity_1hljoeq" targetRef="TextAnnotation_1ys83yq" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0xjb3la">
<bpmndi:BPMNShape id="Participant_0ozb2sp_di" bpmnElement="Participant_0ozb2sp" isHorizontal="true">
<dc:Bounds x="190" y="80" width="550" height="370" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1s1s7a1_di" bpmnElement="Lane_1s1s7a1" isHorizontal="true">
<dc:Bounds x="220" y="80" width="520" height="245" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1m47545_di" bpmnElement="Lane_1m47545" isHorizontal="true">
<dc:Bounds x="220" y="325" width="520" height="125" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_1ys83yq_di" bpmnElement="TextAnnotation_1ys83yq">
<dc:Bounds x="250" y="100" width="130.6238034460753" height="68.28334396936822" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0a7090c_di" bpmnElement="Flow_0a7090c">
<di:waypoint x="276" y="260" />
<di:waypoint x="330" y="260" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gp4zfd_di" bpmnElement="Flow_1gp4zfd">
<di:waypoint x="430" y="390" />
<di:waypoint x="485" y="390" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vnghsi_di" bpmnElement="Flow_0vnghsi">
<di:waypoint x="510" y="365" />
<di:waypoint x="510" y="300" />
<bpmndi:BPMNLabel>
<dc:Bounds x="520" y="334" width="40" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1g38q6b_di" bpmnElement="Flow_1g38q6b">
<di:waypoint x="535" y="390" />
<di:waypoint x="680" y="390" />
<di:waypoint x="680" y="278" />
<bpmndi:BPMNLabel>
<dc:Bounds x="585" y="372" width="46" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hcpt7c_di" bpmnElement="Flow_1hcpt7c">
<di:waypoint x="380" y="300" />
<di:waypoint x="380" y="350" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_070gq5r_di" bpmnElement="Flow_070gq5r">
<di:waypoint x="510" y="220" />
<di:waypoint x="510" y="160" />
<di:waypoint x="380" y="160" />
<di:waypoint x="380" y="220" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="240" y="242" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0xcxw40_di" bpmnElement="Activity_1hljoeq">
<dc:Bounds x="330" y="220" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1fkgc4u_di" bpmnElement="Gateway_1fkgc4u" isMarkerVisible="true">
<dc:Bounds x="485" y="365" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0lscajc_di" bpmnElement="Event_0lscajc">
<dc:Bounds x="662" y="242" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1jfdeta_di" bpmnElement="Activity_19ccxoj">
<dc:Bounds x="460" y="220" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zc7cgy_di" bpmnElement="Activity_14eor1x">
<dc:Bounds x="330" y="350" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Association_1kcb9ou_di" bpmnElement="Association_1kcb9ou">
<di:waypoint x="359" y="220" />
<di:waypoint x="333" y="168" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,41 +1,6 @@
{
"entries": [
{
"attributes": {
"cn": [
"Laura Barnes (lb3dp)"
],
"displayName": "Laura Barnes",
"givenName": [
"Laura"
],
"mail": [
"lb3dp@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E0:Associate Professor of Systems and Information Engineering"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Sys and Environment"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
},
"dn": "uid=lb3dp,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
@ -76,41 +41,6 @@
}
},
{
"attributes": {
"cn": [
"Dan Funk (dhf8r)"
],
"displayName": "Dan Funk",
"givenName": [
"Dan"
],
"mail": [
"dhf8r@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E42:He's a hoopy frood"
],
"uvaDisplayDepartment": [
"E0:EN-Eng Study of Parallel Universes"
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
},
"dn": "uid=dhf8r,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
@ -149,7 +79,46 @@
"Staff"
]
}
},
{
"dn": "uid=lje5u,ou=People,o=University of Virginia,c=US",
"raw": {
"cn": [
"Elder, Lori J (lje5u)"
],
"displayName": [
"Lori Elder"
],
"givenName": [
"Lori"
],
"mail": [
"lje5u@virginia.edu"
],
"objectClass": [
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
"uvaPerson",
"uidObject"
],
"telephoneNumber": [
"+1 (434) 924-1723"
],
"title": [
"E42:The vision"
],
"uvaDisplayDepartment": [
"E0:EN-Phy Anything could go here."
],
"uvaPersonIAMAffiliation": [
"faculty"
],
"uvaPersonSponsoredType": [
"Staff"
]
}
}
]
}

155
tests/data/roles/roles.bpmn Normal file
View File

@ -0,0 +1,155 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0ybr9ph" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.0.0">
<bpmn:collaboration id="Collaboration_0xjb3la">
<bpmn:participant id="Participant_0ozb2sp" name="pool_name" processRef="Process_1aebbrh" />
</bpmn:collaboration>
<bpmn:process id="Process_1aebbrh" isExecutable="true">
<bpmn:laneSet id="LaneSet_0ilprw6">
<bpmn:lane id="Lane_1s1s7a1">
<bpmn:flowNodeRef>StartEvent_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_1hljoeq</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Event_0lscajc</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_19ccxoj</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="Lane_1m47545" name="supervisor">
<bpmn:flowNodeRef>Gateway_1fkgc4u</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Activity_14eor1x</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0a7090c</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Activity_1hljoeq" name="Request Approval" camunda:formKey="request_form_key">
<bpmn:documentation># Answer me these questions 3, ere the other side you see!</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="favorite_color" label="What is your favorite color?" type="string" defaultValue="Yellow" />
<camunda:formField id="quest" label="What is your quest?" type="string" defaultValue="To seek the holly Grail!" />
<camunda:formField id="swallow_speed" label="What is the air speed velocity of an unladen swallow?" type="string" defaultValue="About 24 miles per hour" />
<camunda:formField id="supervisor" label="Please enter the UVA Id of your supervisor" type="string" defaultValue="dhf8r" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0a7090c</bpmn:incoming>
<bpmn:incoming>Flow_070gq5r</bpmn:incoming>
<bpmn:outgoing>Flow_1hcpt7c</bpmn:outgoing>
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_1fkgc4u">
<bpmn:incoming>Flow_1gp4zfd</bpmn:incoming>
<bpmn:outgoing>Flow_0vnghsi</bpmn:outgoing>
<bpmn:outgoing>Flow_1g38q6b</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="Event_0lscajc">
<bpmn:documentation># Your responses were approved!
Gosh! you must really know a lot about colors and swallows and stuff!
Your supervisor provided the following feedback:
{{feedback}}
You are all done! WARNING: If you go back and reanswer the questions it will create a new approval request.</bpmn:documentation>
<bpmn:incoming>Flow_1g38q6b</bpmn:incoming>
</bpmn:endEvent>
<bpmn:manualTask id="Activity_19ccxoj" name="Review Feedback">
<bpmn:documentation># Your Request was rejected
Perhaps you don't know the right answer to one of the questions.
Your Supervisor provided the following feedback:
{{feedback}}
Please press save to re-try the questions, and submit your responses again.</bpmn:documentation>
<bpmn:incoming>Flow_0vnghsi</bpmn:incoming>
<bpmn:outgoing>Flow_070gq5r</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:sequenceFlow id="Flow_0a7090c" sourceRef="StartEvent_1" targetRef="Activity_1hljoeq" />
<bpmn:sequenceFlow id="Flow_1gp4zfd" sourceRef="Activity_14eor1x" targetRef="Gateway_1fkgc4u" />
<bpmn:sequenceFlow id="Flow_0vnghsi" name="rejected" sourceRef="Gateway_1fkgc4u" targetRef="Activity_19ccxoj">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==False</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1g38q6b" name="approved" sourceRef="Gateway_1fkgc4u" targetRef="Event_0lscajc">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">approval==True</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1hcpt7c" sourceRef="Activity_1hljoeq" targetRef="Activity_14eor1x" />
<bpmn:sequenceFlow id="Flow_070gq5r" sourceRef="Activity_19ccxoj" targetRef="Activity_1hljoeq" />
<bpmn:userTask id="Activity_14eor1x" name="Approve Responses" camunda:formKey="approval_form_key">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="approval" label="I approve of this information" type="boolean" />
<camunda:formField id="feedback" label="Feedback" type="string" defaultValue="Please provide any feedback you have here." />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1hcpt7c</bpmn:incoming>
<bpmn:outgoing>Flow_1gp4zfd</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0xjb3la">
<bpmndi:BPMNShape id="Participant_0ozb2sp_di" bpmnElement="Participant_0ozb2sp" isHorizontal="true">
<dc:Bounds x="190" y="80" width="550" height="310" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1m47545_di" bpmnElement="Lane_1m47545" isHorizontal="true">
<dc:Bounds x="220" y="265" width="520" height="125" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Lane_1s1s7a1_di" bpmnElement="Lane_1s1s7a1" isHorizontal="true">
<dc:Bounds x="220" y="80" width="520" height="185" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_070gq5r_di" bpmnElement="Flow_070gq5r">
<di:waypoint x="510" y="160" />
<di:waypoint x="510" y="100" />
<di:waypoint x="380" y="100" />
<di:waypoint x="380" y="160" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hcpt7c_di" bpmnElement="Flow_1hcpt7c">
<di:waypoint x="380" y="240" />
<di:waypoint x="380" y="290" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1g38q6b_di" bpmnElement="Flow_1g38q6b">
<di:waypoint x="535" y="330" />
<di:waypoint x="680" y="330" />
<di:waypoint x="680" y="218" />
<bpmndi:BPMNLabel>
<dc:Bounds x="585" y="312" width="46" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vnghsi_di" bpmnElement="Flow_0vnghsi">
<di:waypoint x="510" y="305" />
<di:waypoint x="510" y="240" />
<bpmndi:BPMNLabel>
<dc:Bounds x="520" y="274" width="40" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gp4zfd_di" bpmnElement="Flow_1gp4zfd">
<di:waypoint x="430" y="330" />
<di:waypoint x="485" y="330" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0a7090c_di" bpmnElement="Flow_0a7090c">
<di:waypoint x="276" y="200" />
<di:waypoint x="330" y="200" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="240" y="182" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0xcxw40_di" bpmnElement="Activity_1hljoeq">
<dc:Bounds x="330" y="160" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1fkgc4u_di" bpmnElement="Gateway_1fkgc4u" isMarkerVisible="true">
<dc:Bounds x="485" y="305" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0lscajc_di" bpmnElement="Event_0lscajc">
<dc:Bounds x="662" y="182" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1jfdeta_di" bpmnElement="Activity_19ccxoj">
<dc:Bounds x="460" y="160" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0zc7cgy_di" bpmnElement="Activity_14eor1x">
<dc:Bounds x="330" y="290" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -8,7 +8,7 @@ from crc import session, app
from crc.models.protocol_builder import ProtocolBuilderStatus, \
ProtocolBuilderStudySchema
from crc.models.approval import ApprovalStatus
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyModel, StudySchema
from crc.models.workflow import WorkflowSpecModel, WorkflowModel
from crc.services.file_service import FileService

View File

@ -9,80 +9,10 @@ from crc import session, app
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType, TaskSchema
from crc.models.file import FileModelSchema
from crc.models.workflow import WorkflowStatus
from crc.services.workflow_service import WorkflowService
from crc.models.stats import TaskEventModel
class TestTasksApi(BaseTest):
def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False):
rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' %
(workflow.id, str(soft_reset), str(hard_reset)),
headers=self.logged_in_headers(),
content_type="application/json")
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
workflow_api = WorkflowApiSchema().load(json_data)
self.assertEqual(workflow.workflow_spec_id, workflow_api.workflow_spec_id)
return workflow_api
def complete_form(self, workflow_in, task_in, dict_data, error_code = None):
prev_completed_task_count = workflow_in.completed_tasks
if isinstance(task_in, dict):
task_id = task_in["id"]
else:
task_id = task_in.id
rv = self.app.put('/v1.0/workflow/%i/task/%s/data' % (workflow_in.id, task_id),
headers=self.logged_in_headers(),
content_type="application/json",
data=json.dumps(dict_data))
if error_code:
self.assert_failure(rv, error_code=error_code)
return
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
# Assure stats are updated on the model
workflow = WorkflowApiSchema().load(json_data)
# The total number of tasks may change over time, as users move through gateways
# branches may be pruned. As we hit parallel Multi-Instance new tasks may be created...
self.assertIsNotNone(workflow.total_tasks)
self.assertEqual(prev_completed_task_count + 1, workflow.completed_tasks)
# Assure a record exists in the Task Events
task_events = session.query(TaskEventModel) \
.filter_by(workflow_id=workflow.id) \
.filter_by(task_id=task_id) \
.order_by(TaskEventModel.date.desc()).all()
self.assertGreater(len(task_events), 0)
event = task_events[0]
self.assertIsNotNone(event.study_id)
self.assertEqual("dhf8r", event.user_uid)
self.assertEqual(workflow.id, event.workflow_id)
self.assertEqual(workflow.workflow_spec_id, event.workflow_spec_id)
self.assertEqual(workflow.spec_version, event.spec_version)
self.assertEqual(WorkflowService.TASK_ACTION_COMPLETE, event.action)
self.assertEqual(task_in.id, task_id)
self.assertEqual(task_in.name, event.task_name)
self.assertEqual(task_in.title, event.task_title)
self.assertEqual(task_in.type, event.task_type)
self.assertEqual("COMPLETED", event.task_state)
# Not sure what vodoo is happening inside of marshmallow to get me in this state.
if isinstance(task_in.multi_instance_type, MultiInstanceType):
self.assertEqual(task_in.multi_instance_type.value, event.mi_type)
else:
self.assertEqual(task_in.multi_instance_type, event.mi_type)
self.assertEqual(task_in.multi_instance_count, event.mi_count)
self.assertEqual(task_in.multi_instance_index, event.mi_index)
self.assertEqual(task_in.process_name, event.process_name)
self.assertIsNotNone(event.date)
# Assure that there is data in the form_data
self.assertIsNotNone(event.form_data)
workflow = WorkflowApiSchema().load(json_data)
return workflow
def assert_options_populated(self, results, lookup_data_keys):
option_keys = ['value', 'label', 'data']
self.assertIsInstance(results, list)

202
tests/test_user_roles.py Normal file
View File

@ -0,0 +1,202 @@
import json
from tests.base_test import BaseTest
from crc.models.workflow import WorkflowStatus
from crc import db
from crc.api.common import ApiError
from crc.models.task_event import TaskEventModel, TaskEventSchema
from crc.services.workflow_service import WorkflowService
class TestTasksApi(BaseTest):
def test_raise_error_if_role_does_not_exist_in_data(self):
workflow = self.create_workflow('roles', as_user="lje5u")
workflow_api = self.get_workflow_api(workflow, user_uid="lje5u")
data = workflow_api.next_task.data
# User lje5u can complete the first task
self.complete_form(workflow, workflow_api.next_task, data, user_uid="lje5u")
# The next task is a supervisor task, and should raise an error if the role
# information is not in the task data.
workflow_api = self.get_workflow_api(workflow, user_uid="lje5u")
data = workflow_api.next_task.data
data["approved"] = True
result = self.complete_form(workflow, workflow_api.next_task, data, user_uid="lje5u",
error_code="permission_denied")
def test_validation_of_workflow_fails_if_workflow_does_not_define_user_for_lane(self):
error = None
try:
workflow = self.create_workflow('invalid_roles', as_user="lje5u")
WorkflowService.test_spec(workflow.workflow_spec_id)
except ApiError as ae:
error = ae
self.assertIsNotNone(error, "An error should be raised.")
self.assertEquals("invalid_role", error.code)
def test_raise_error_if_user_does_not_have_the_correct_role(self):
submitter = self.create_user(uid='lje5u')
supervisor = self.create_user(uid='lb3dp')
workflow = self.create_workflow('roles', as_user=submitter.uid)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
# User lje5u can complete the first task, and set her supervisor
data = workflow_api.next_task.data
data['supervisor'] = supervisor.uid
self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
# But she can not complete the supervisor role.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
data = workflow_api.next_task.data
data["approval"] = True
result = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid,
error_code="permission_denied")
# Only her supervisor can do that.
self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
def test_nav_includes_lanes(self):
submitter = self.create_user(uid='lje5u')
workflow = self.create_workflow('roles', as_user=submitter.uid)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals("supervisor", nav[1]['lane'])
def test_get_outstanding_tasks_awaiting_current_user(self):
submitter = self.create_user(uid='lje5u')
supervisor = self.create_user(uid='lb3dp')
workflow = self.create_workflow('roles', as_user=submitter.uid)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
# User lje5u can complete the first task, and set her supervisor
data = workflow_api.next_task.data
data['supervisor'] = supervisor.uid
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
# At this point there should be a task_log with an action of Lane Change on it for
# the supervisor.
task_logs = db.session.query(TaskEventModel). \
filter(TaskEventModel.user_uid == supervisor.uid). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).all()
self.assertEquals(1, len(task_logs))
# A call to the /task endpoint as the supervisor user should return a list of
# tasks that need their attention.
rv = self.app.get('/v1.0/task_events?action=ASSIGNMENT',
headers=self.logged_in_headers(supervisor),
content_type="application/json")
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
tasks = TaskEventSchema(many=True).load(json_data)
self.assertEquals(1, len(tasks))
self.assertEquals(workflow.id, tasks[0]['workflow']['id'])
self.assertEquals(workflow.study.id, tasks[0]['study']['id'])
# Assure we can say something sensible like:
# You have a task called "Approval" to be completed in the "Supervisor Approval" workflow
# for the study 'Why dogs are stinky' managed by user "Jane Smith (js42x)",
# please check here to complete the task.
# Display name isn't set in the tests, so just checking name, but the full workflow details are included.
# I didn't delve into the full user details to keep things decoupled from ldap, so you just get the
# uid back, but could query to get the full entry.
self.assertEquals("roles", tasks[0]['workflow']['name'])
self.assertEquals("Beer consumption in the bipedal software engineer", tasks[0]['study']['title'])
self.assertEquals("lje5u", tasks[0]['study']['user_uid'])
# Completing the next step of the workflow will close the task.
data['approval'] = True
self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
def test_navigation_and_current_task_updates_through_workflow(self):
submitter = self.create_user(uid='lje5u')
supervisor = self.create_user(uid='lb3dp')
workflow = self.create_workflow('roles', as_user=submitter.uid)
# Navigation as Submitter with ready task.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('READY', nav[0]['state']) # First item is ready, no progress yet.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('NOOP', nav[3]['state']) # Approved Path, has no operation
self.assertEquals('NOOP', nav[4]['state']) # Rejected Path, has no operation.
self.assertEquals('READY', workflow_api.next_task.state)
# Navigation as Submitter after handoff to supervisor
data = workflow_api.next_task.data
data['supervisor'] = supervisor.uid
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals('COMPLETED', nav[0]['state']) # First item is ready, no progress yet.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('LOCKED', workflow_api.next_task.state)
# In the event the next task is locked, we should say something sensible here.
# It is possible to look at the role of the task, and say The next task "TASK TITLE" will
# be handled by 'dhf8r', who is full-filling the role of supervisor. the Task Data
# is guaranteed to have a supervisor attribute in it that will contain the users uid, which
# could be looked up through an ldap service.
self.assertEquals('supervisor', workflow_api.next_task.lane)
# Navigation as Supervisor
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('READY', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
data = workflow_api.next_task.data
data["approval"] = False
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
# Navigation as Supervisor, after completing task.
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('COMPLETED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('COMPLETED', nav[2]['state']) # third item is a gateway, and is now complete.
self.assertEquals('LOCKED', workflow_api.next_task.state)
# Navigation as Submitter, coming back in to a rejected workflow to view the rejection message.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('COMPLETED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
# Navigation as Submitter, re-completing the original request a second time, and sending it for review.
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('COMPLETED', nav[0]['state']) # We still have some issues here, the navigation will be off when looping back.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
data["favorite_color"] = "blue"
data["quest"] = "to seek the holy grail"
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
self.assertEquals('LOCKED', workflow_api.next_task.state)
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
self.assertEquals('READY', workflow_api.next_task.state)
data = workflow_api.next_task.data
data["approval"] = True
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
self.assertEquals('LOCKED', workflow_api.next_task.state)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
self.assertEquals('COMPLETED', workflow_api.next_task.state)
self.assertEquals('EndEvent', workflow_api.next_task.type) # Are are at the end.
self.assertEquals(WorkflowStatus.complete, workflow_api.status)

View File

@ -371,4 +371,16 @@ class TestWorkflowProcessor(BaseTest):
self._populate_form_with_random_data(task)
def test_get_role_by_name(self):
self.load_example_data()
workflow_spec_model = self.load_test_spec("roles")
study = session.query(StudyModel).first()
processor = self.get_processor(study, workflow_spec_model)
processor.do_engine_steps()
tasks = processor.next_user_tasks()
task = tasks[0]
self._populate_form_with_random_data(task)
processor.complete_task(task)
supervisor_task = processor.next_user_tasks()[0]
self.assertEquals("supervisor", supervisor_task.task_spec.lane)

View File

@ -1,4 +1,5 @@
import json
import unittest
from tests.base_test import BaseTest
@ -7,7 +8,7 @@ from crc.services.workflow_service import WorkflowService
from SpiffWorkflow import Task as SpiffTask, WorkflowException
from example_data import ExampleDataLoader
from crc import db
from crc.models.stats import TaskEventModel
from crc.models.task_event import TaskEventModel
from crc.models.api_models import Task
from crc.api.common import ApiError

View File

@ -1,4 +1,5 @@
import json
import unittest
from unittest.mock import patch
from tests.base_test import BaseTest
@ -51,9 +52,6 @@ class TestWorkflowSpecValidation(BaseTest):
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()