"Reset" should not mean "re-start". Calling the reset_workflow script will now set the workflow to an "unstarted" state with no outstanding tasks, no json-state structure stored. The workflow is not yet running.

Also:
* Assured that arguments are consistent (we always seem to use workflow_spec_id, so I made sure we use that consistently.
* Don't require named parameters - so it's cool to call it like: reset_workflow('my_workflow_id')
* Task Actions (ie create, assign, etc...) are now an enumeration in the models, and not static variables on Workflow Service, so we can reference them consistently from anywhere.
* Removed some repetitive code
* Always try to validate as much as possible in the scripts to save folks time debugging.
*
This commit is contained in:
Dan 2022-04-15 15:36:23 -04:00
parent 0072f4ecea
commit 5e54c90b47
18 changed files with 142 additions and 135 deletions

View File

@ -6,7 +6,7 @@ from crc import session
from crc.api.common import ApiError, ApiErrorSchema from crc.api.common import ApiError, ApiErrorSchema
from crc.models.api_models import WorkflowApiSchema from crc.models.api_models import WorkflowApiSchema
from crc.models.study import StudyModel, WorkflowMetadata, StudyStatus from crc.models.study import StudyModel, WorkflowMetadata, StudyStatus
from crc.models.task_event import TaskEventModel, TaskEvent, TaskEventSchema from crc.models.task_event import TaskEventModel, TaskEvent, TaskEventSchema, TaskAction
from crc.models.workflow import WorkflowModel, WorkflowSpecInfoSchema, WorkflowSpecCategorySchema from crc.models.workflow import WorkflowModel, WorkflowSpecInfoSchema, WorkflowSpecCategorySchema
from crc.services.error_service import ValidationErrorService from crc.services.error_service import ValidationErrorService
from crc.services.lookup_service import LookupService from crc.services.lookup_service import LookupService
@ -205,8 +205,10 @@ def get_workflow(workflow_id, do_engine_steps=True):
def restart_workflow(workflow_id, clear_data=False, delete_files=False): def restart_workflow(workflow_id, clear_data=False, delete_files=False):
"""Restart a workflow with the latest spec. """Restart a workflow with the latest spec.
Clear data allows user to restart the workflow without previous data.""" Clear data allows user to restart the workflow without previous data."""
# fixme: remove delete_files arg, clear_data is the only one respected.
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first() workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor.reset(workflow_model, clear_data=clear_data, delete_files=delete_files) WorkflowProcessor.reset(workflow_model, clear_data=clear_data)
processor = WorkflowProcessor(workflow_model)
processor.do_engine_steps() processor.do_engine_steps()
processor.save() processor.save()
WorkflowService.update_task_assignments(processor) WorkflowService.update_task_assignments(processor)
@ -269,7 +271,7 @@ 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. spiff_task.reset_token({}, reset_data=True) # Don't try to copy the existing data back into this task.
processor.save() processor.save()
WorkflowService.log_task_action(user_uid, processor, spiff_task, WorkflowService.TASK_ACTION_TOKEN_RESET) WorkflowService.log_task_action(user_uid, processor, spiff_task, TaskAction.TOKEN_RESET.value)
WorkflowService.update_task_assignments(processor) WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor, spiff_task) workflow_api_model = WorkflowService.processor_to_workflow_api(processor, spiff_task)
@ -327,7 +329,7 @@ def __update_task(processor, task, data, user):
# Log the action before doing the engine steps, as doing so could effect the state of the task # Log the action before doing the engine steps, as doing so could effect the state of the task
# the workflow could wrap around in the ngine steps, and the task could jump from being completed to # the workflow could wrap around in the ngine steps, and the task could jump from being completed to
# another state. What we are logging here is the completion. # another state. What we are logging here is the completion.
WorkflowService.log_task_action(user.uid, processor, task, WorkflowService.TASK_ACTION_COMPLETE) WorkflowService.log_task_action(user.uid, processor, task, TaskAction.COMPLETE.value)
processor.do_engine_steps() processor.do_engine_steps()
processor.save() processor.save()

View File

@ -1,3 +1,5 @@
import enum
from marshmallow import INCLUDE, fields from marshmallow import INCLUDE, fields
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
@ -7,6 +9,13 @@ from crc.models.workflow import WorkflowModel
from crc.services.ldap_service import LdapService from crc.services.ldap_service import LdapService
from sqlalchemy import func from sqlalchemy import func
class TaskAction(enum.Enum):
COMPLETE = "COMPLETE"
TOKEN_RESET = "TOKEN_RESET"
HARD_RESET = "HARD_RESET"
SOFT_RESET = "SOFT_RESET"
ASSIGNMENT = "ASSIGNMENT" # Whenever the lane changes between tasks we assign the task to specific user.
class TaskEventModel(db.Model): class TaskEventModel(db.Model):
__tablename__ = 'task_event' __tablename__ = 'task_event'

View File

@ -2,7 +2,7 @@ from crc import session
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.data_store import DataStoreModel from crc.models.data_store import DataStoreModel
from crc.models.file import FileModel from crc.models.file import FileModel
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel, TaskAction
from crc.scripts.script import Script from crc.scripts.script import Script
from crc.services.document_service import DocumentService from crc.services.document_service import DocumentService
from crc.services.user_file_service import UserFileService from crc.services.user_file_service import UserFileService
@ -36,7 +36,7 @@ class DeleteTaskData(Script):
# delete task events # delete task events
session.query(TaskEventModel).filter(TaskEventModel.workflow_id == workflow_id).filter( session.query(TaskEventModel).filter(TaskEventModel.workflow_id == workflow_id).filter(
TaskEventModel.study_id == study_id).filter(TaskEventModel.task_name == task_spec_name).filter_by( TaskEventModel.study_id == study_id).filter(TaskEventModel.task_name == task_spec_name).filter_by(
action=WorkflowService.TASK_ACTION_COMPLETE).delete() action=TaskAction.COMPLETE.value).delete()
files_to_delete = session.query(FileModel). \ files_to_delete = session.query(FileModel). \
filter(FileModel.workflow_id == workflow_id). \ filter(FileModel.workflow_id == workflow_id). \

View File

@ -9,34 +9,42 @@ from crc.services.workflow_spec_service import WorkflowSpecService
class ResetWorkflow(Script): class ResetWorkflow(Script):
def get_description(self): def get_description(self):
return """Reset a workflow. Run by master workflow. return """Reset a workflow. Run by mas vftgv ter workflow.
Designed for completed workflows where we need to force rerunning the workflow. Designed for completed workflows where we need to force rerunning the workflow.
I.e., a new PI""" I.e., a new PI"""
def get_spec(self, *args, **kwargs):
workflow_spec_id = None
if 'workflow_spec_id' in kwargs.keys():
workflow_spec_id = kwargs['workflow_spec_id']
elif len(args) > 0:
workflow_spec_id = args[0]
if not workflow_spec_id:
raise ApiError(code='missing_workflow_id',
message='Reset workflow requires a workflow_spec_id')
workflow_spec = WorkflowSpecService().get_spec(workflow_spec_id)
if not workflow_spec:
raise ApiError(code='missing_workflow_spec',
message=f'No workflow spec found with the \
id: {workflow_spec_id}')
return workflow_spec
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
return hasattr(kwargs, 'reset_id') self.get_spec(*args, **kwargs) # Just assure we can find the workflow spec.
def do_task(self, task, study_id, workflow_id, *args, **kwargs): def do_task(self, task, study_id, workflow_id, *args, **kwargs):
if 'clear_data' in kwargs.keys():
if 'reset_id' in kwargs.keys(): clear_data = bool(kwargs['clear_data'])
reset_id = kwargs['reset_id']
workflow_spec = WorkflowSpecService().get_spec(reset_id)
if workflow_spec:
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(
workflow_spec_id=workflow_spec.id,
study_id=study_id).first()
if workflow_model:
workflow_processor = WorkflowProcessor.reset(workflow_model, clear_data=False, delete_files=False)
return workflow_processor
else:
raise ApiError(code='missing_workflow_model',
message=f'No WorkflowModel returned. \
workflow_spec_id: {workflow_spec.id} \
study_id: {study_id}')
else:
raise ApiError(code='missing_workflow_spec',
message=f'No WorkflowSpecModel returned. \
id: {workflow_id}')
else: else:
raise ApiError(code='missing_workflow_id', clear_data = False
message='Reset workflow requires a workflow id')
workflow_spec = self.get_spec(*args, **kwargs)
if workflow_spec:
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(
workflow_spec_id=workflow_spec.id,
study_id=study_id).first()
if workflow_model:
WorkflowProcessor.reset(workflow_model, clear_data=clear_data)

View File

@ -9,67 +9,40 @@ from crc.services.workflow_service import WorkflowService
class StartWorkflow(Script): class StartWorkflow(Script):
@staticmethod
def get_workflow(workflow_id):
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model)
processor.do_engine_steps()
processor.save()
WorkflowService.update_task_assignments(processor)
workflow_api_model = WorkflowService.processor_to_workflow_api(processor)
return WorkflowApiSchema().dump(workflow_api_model)
def get_description(self): def get_description(self):
return """Script to start a workflow programmatically. return """Script to start a workflow programmatically.
It requires a workflow_spec_id. It requires a workflow_spec_id.
It accepts the workflow_spec_id as a positional argument It accepts the workflow_spec_id as a positional argument
or with the keyword 'workflow_spec_id'""" or with the keyword 'workflow_spec_id'"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): def get_workflow(self, study_id, *args, **kwargs):
if len(args) == 1 or 'workflow_spec_id' in kwargs: if len(args) == 1 or 'workflow_spec_id' in kwargs:
if 'workflow_spec_id' in kwargs: if 'workflow_spec_id' in kwargs:
workflow_spec_id = kwargs['workflow_spec_id'] workflow_spec_id = kwargs['workflow_spec_id']
else: else:
workflow_spec_id = args[0] workflow_spec_id = args[0]
workflow_api = WorkflowApi(1234,
WorkflowStatus('user_input_required'),
'next_task',
'navigation',
workflow_spec_id,
'total_tasks',
'completed_tasks',
'last_updated',
'is_review',
'title',
study_id)
return WorkflowApiSchema().dump(workflow_api)
else: else:
raise ApiError(code='missing_parameter', raise ApiError(code='missing_parameter',
message=f'The start_workflow script requires a workflow id') message=f'The start_workflow script requires a workflow id')
workflow = session.query(WorkflowModel).\
filter(WorkflowModel.study_id==study_id).\
filter(WorkflowModel.workflow_spec_id==workflow_spec_id).\
first()
if not(workflow):
raise ApiError(code='unknown_workflow',
message=f"We could not find a workflow with workflow_spec_id '{workflow_spec_id}'.")
return workflow
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.get_workflow(study_id, *args, **kwargs)
def do_task(self, task, study_id, workflow_id, *args, **kwargs): def do_task(self, task, study_id, workflow_id, *args, **kwargs):
if len(args) == 1 or 'workflow_spec_id' in kwargs: workflow_model = self.get_workflow(study_id, *args, **kwargs)
if 'workflow_spec_id' in kwargs: processor = WorkflowProcessor(workflow_model)
workflow_spec_id = kwargs['workflow_spec_id'] processor.do_engine_steps()
else: processor.save()
workflow_spec_id = args[0] WorkflowService.update_task_assignments(processor)
workflow = session.query(WorkflowModel).\
filter(WorkflowModel.study_id==study_id).\
filter(WorkflowModel.workflow_spec_id==workflow_spec_id).\
first()
if workflow:
workflow_api = self.get_workflow(workflow.id)
return workflow_api
else:
raise ApiError(code='unknown_workflow',
message=f"We could not find a workflow with workflow_spec_id '{workflow_spec_id}'.")
else:
raise ApiError(code='missing_parameter',
message=f'The start_workflow script requires a workflow id')

View File

@ -20,7 +20,7 @@ from SpiffWorkflow.specs import WorkflowSpec
from crc import session from crc import session
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.file import FileModel, FileType, File from crc.models.file import FileModel, FileType, File
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel, TaskAction
from crc.models.user import UserModelSchema from crc.models.user import UserModelSchema
from crc.models.workflow import WorkflowStatus, WorkflowModel, WorkflowSpecInfo from crc.models.workflow import WorkflowStatus, WorkflowModel, WorkflowSpecInfo
from crc.scripts.script import Script from crc.scripts.script import Script
@ -171,7 +171,13 @@ class WorkflowProcessor(object):
task.data['current_user'] = current_user_data task.data['current_user'] = current_user_data
@staticmethod @staticmethod
def reset(workflow_model, clear_data=False, delete_files=False): def reset(workflow_model, clear_data=False):
"""Resets the workflow back to an unstarted state - where nothing has
happened yet. If clear_data is set to false, then the information
previously used in forms will be re-populated when the form is re-
displayed, and any files that were updated will remain in place, otherwise
files will also be cleared out."""
# Try to execute a cancel notify # Try to execute a cancel notify
try: try:
bpmn_workflow = WorkflowProcessor.__get_bpmn_workflow(workflow_model) bpmn_workflow = WorkflowProcessor.__get_bpmn_workflow(workflow_model)
@ -182,19 +188,27 @@ class WorkflowProcessor(object):
f" state. An %s error occured with the following information: %s" % f" state. An %s error occured with the following information: %s" %
(workflow_model.id, e.__class__.__name__, str(e))) (workflow_model.id, e.__class__.__name__, str(e)))
workflow_model.bpmn_workflow_json = None workflow_model.bpmn_workflow_json = None
workflow_model.status = WorkflowStatus.not_started
# clear out any task assignments
session.query(TaskEventModel). \
filter(TaskEventModel.workflow_id == workflow_model.id). \
filter(TaskEventModel.action == TaskAction.ASSIGNMENT.value).delete()
if clear_data: if clear_data:
# Clear form_data from task_events # Clear out data in previous task events
task_events = session.query(TaskEventModel). \ task_events = session.query(TaskEventModel). \
filter(TaskEventModel.workflow_id == workflow_model.id).all() filter(TaskEventModel.workflow_id == workflow_model.id).all()
for task_event in task_events: for task_event in task_events:
task_event.form_data = {} task_event.form_data = {}
session.add(task_event) session.add(task_event)
if delete_files: # Remove any uploaded files.
files = FileModel.query.filter(FileModel.workflow_id == workflow_model.id).all() files = FileModel.query.filter(FileModel.workflow_id == workflow_model.id).all()
for file in files: for file in files:
UserFileService.delete_file(file.id) UserFileService.delete_file(file.id)
session.commit() session.commit()
return WorkflowProcessor(workflow_model)
@staticmethod @staticmethod
def __get_bpmn_workflow(workflow_model: WorkflowModel, spec: WorkflowSpec = None, validate_only=False): def __get_bpmn_workflow(workflow_model: WorkflowModel, spec: WorkflowSpec = None, validate_only=False):

View File

@ -27,7 +27,7 @@ from crc.models.api_models import Task, MultiInstanceType, WorkflowApi
from crc.models.file import LookupDataModel, FileModel, File, FileSchema from crc.models.file import LookupDataModel, FileModel, File, FileSchema
from crc.models.ldap import LdapModel from crc.models.ldap import LdapModel
from crc.models.study import StudyModel from crc.models.study import StudyModel
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel, TaskAction
from crc.models.user import UserModel from crc.models.user import UserModel
from crc.models.workflow import WorkflowModel, WorkflowStatus from crc.models.workflow import WorkflowModel, WorkflowStatus
from crc.services.data_store_service import DataStoreBase from crc.services.data_store_service import DataStoreBase
@ -45,11 +45,6 @@ from flask import g
class WorkflowService(object): class WorkflowService(object):
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. TASK_STATE_LOCKED = "LOCKED" # When the task belongs to a different user.
@ -723,7 +718,7 @@ class WorkflowService(object):
query = db.session.query(TaskEventModel) \ query = db.session.query(TaskEventModel) \
.filter_by(workflow_id=workflow_id) \ .filter_by(workflow_id=workflow_id) \
.filter_by(task_name=spiff_task.task_spec.name) \ .filter_by(task_name=spiff_task.task_spec.name) \
.filter_by(action=WorkflowService.TASK_ACTION_COMPLETE) .filter_by(action=TaskAction.COMPLETE.value)
if hasattr(spiff_task, 'internal_data') and 'runtimes' in spiff_task.internal_data: if hasattr(spiff_task, 'internal_data') and 'runtimes' in spiff_task.internal_data:
query = query.filter_by(mi_index=spiff_task.internal_data['runtimes']) query = query.filter_by(mi_index=spiff_task.internal_data['runtimes'])
@ -976,14 +971,14 @@ class WorkflowService(object):
should be called whenever progress is made on a workflow.""" should be called whenever progress is made on a workflow."""
db.session.query(TaskEventModel). \ db.session.query(TaskEventModel). \
filter(TaskEventModel.workflow_id == processor.workflow_model.id). \ filter(TaskEventModel.workflow_id == processor.workflow_model.id). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).delete() filter(TaskEventModel.action == TaskAction.ASSIGNMENT.value).delete()
db.session.commit() db.session.commit()
tasks = processor.get_current_user_tasks() tasks = processor.get_current_user_tasks()
for task in tasks: for task in tasks:
user_ids = WorkflowService.get_users_assigned_to_task(processor, task) user_ids = WorkflowService.get_users_assigned_to_task(processor, task)
for user_id in user_ids: for user_id in user_ids:
WorkflowService.log_task_action(user_id, processor, task, WorkflowService.TASK_ACTION_ASSIGNMENT) WorkflowService.log_task_action(user_id, processor, task, TaskAction.ASSIGNMENT.value)
@staticmethod @staticmethod
def get_users_assigned_to_task(processor, spiff_task) -> List[str]: def get_users_assigned_to_task(processor, spiff_task) -> List[str]:
@ -1121,13 +1116,4 @@ class WorkflowService(object):
db.session.commit() db.session.commit()
return workflow_model return workflow_model
@staticmethod
def delete_workflow_spec_task_events(spec_id):
session.query(TaskEventModel).filter(TaskEventModel.workflow_spec_id == spec_id).delete()
session.commit()
@staticmethod
def delete_workflow_spec_workflow_models(spec_id):
for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id):
StudyService.delete_workflow(workflow.id)

5
package-lock.json generated
View File

@ -1,3 +1,6 @@
{ {
"lockfileVersion": 1 "name": "cr-connect-workflow",
"lockfileVersion": 2,
"requires": true,
"packages": {}
} }

View File

@ -15,7 +15,7 @@ from flask import g
from crc import app, db, session from crc import app, db, session
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType from crc.models.api_models import WorkflowApiSchema, MultiInstanceType
from crc.models.file import FileModel, CONTENT_TYPES from crc.models.file import FileModel, CONTENT_TYPES
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel, TaskAction
from crc.models.study import StudyModel, StudyStatus, ProgressStatus from crc.models.study import StudyModel, StudyStatus, ProgressStatus
from crc.models.user import UserModel from crc.models.user import UserModel
from crc.models.workflow import WorkflowSpecCategory from crc.models.workflow import WorkflowSpecCategory
@ -368,7 +368,7 @@ class BaseTest(unittest.TestCase):
task_events = session.query(TaskEventModel) \ task_events = session.query(TaskEventModel) \
.filter_by(workflow_id=workflow.id) \ .filter_by(workflow_id=workflow.id) \
.filter_by(task_id=task_id) \ .filter_by(task_id=task_id) \
.filter_by(action=WorkflowService.TASK_ACTION_COMPLETE) \ .filter_by(action=TaskAction.COMPLETE.value) \
.order_by(TaskEventModel.date.desc()).all() .order_by(TaskEventModel.date.desc()).all()
self.assertGreater(len(task_events), 0) self.assertGreater(len(task_events), 0)
event = task_events[0] event = task_events[0]
@ -377,7 +377,7 @@ class BaseTest(unittest.TestCase):
self.assertEqual(user_uid, event.user_uid) self.assertEqual(user_uid, event.user_uid)
self.assertEqual(workflow.id, event.workflow_id) self.assertEqual(workflow.id, event.workflow_id)
self.assertEqual(workflow.workflow_spec_id, event.workflow_spec_id) self.assertEqual(workflow.workflow_spec_id, event.workflow_spec_id)
self.assertEqual(WorkflowService.TASK_ACTION_COMPLETE, event.action) self.assertEqual(TaskAction.COMPLETE.value, event.action)
self.assertEqual(task_in.id, task_id) self.assertEqual(task_in.id, task_id)
self.assertEqual(task_in.name, event.task_name) self.assertEqual(task_in.name, event.task_name)
self.assertEqual(task_in.title, event.task_title) self.assertEqual(task_in.title, event.task_title)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0vny0hv" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0"> <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0vny0hv" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.10.0">
<bpmn:process id="Process_ResetWorkflow" name="Reset Workflow" isExecutable="true"> <bpmn:process id="Process_ResetWorkflow" name="Reset Workflow" isExecutable="true">
<bpmn:documentation>Use this process to reset a workflow for the current study. You must enter the name of the workflow. I.e., lower case with underscores.</bpmn:documentation> <bpmn:documentation>Use this process to reset a workflow for the current study. You must enter the name of the workflow. I.e., lower case with underscores.</bpmn:documentation>
<bpmn:startEvent id="StartEvent_1"> <bpmn:startEvent id="StartEvent_1">
@ -15,7 +15,7 @@
<bpmn:userTask id="Task_GetWorkflow" name="Get Workflow" camunda:formKey="WorkflowForm"> <bpmn:userTask id="Task_GetWorkflow" name="Get Workflow" camunda:formKey="WorkflowForm">
<bpmn:extensionElements> <bpmn:extensionElements>
<camunda:formData> <camunda:formData>
<camunda:formField id="workflow_name" label="'Workflow Name'" type="string"> <camunda:formField id="workflow_name" label="&#39;Workflow Name&#39;" type="string" defaultValue="&#34;reset_workflow&#34;">
<camunda:validation> <camunda:validation>
<camunda:constraint name="required" config="True" /> <camunda:constraint name="required" config="True" />
</camunda:validation> </camunda:validation>
@ -28,7 +28,7 @@
<bpmn:scriptTask id="Task_ResetWorkflow" name="Reset Workflow"> <bpmn:scriptTask id="Task_ResetWorkflow" name="Reset Workflow">
<bpmn:incoming>SequenceFlow_1q2ton3</bpmn:incoming> <bpmn:incoming>SequenceFlow_1q2ton3</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0x127gc</bpmn:outgoing> <bpmn:outgoing>SequenceFlow_0x127gc</bpmn:outgoing>
<bpmn:script>value = reset_workflow(workflow_name=workflow_name)</bpmn:script> <bpmn:script>value = reset_workflow(workflow_name)</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:manualTask id="Task_DisplayWorkflow" name="Display Workflow"> <bpmn:manualTask id="Task_DisplayWorkflow" name="Display Workflow">
<bpmn:documentation># Reset Workflow <bpmn:documentation># Reset Workflow
@ -46,28 +46,28 @@
</bpmn:process> </bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_ResetWorkflow"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_ResetWorkflow">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <bpmndi:BPMNEdge id="SequenceFlow_0yy50p2_di" bpmnElement="SequenceFlow_0yy50p2">
<dc:Bounds x="179" y="99" width="36" height="36" /> <di:waypoint x="690" y="117" />
</bpmndi:BPMNShape> <di:waypoint x="752" y="117" />
<bpmndi:BPMNEdge id="SequenceFlow_0i872g2_di" bpmnElement="SequenceFlow_0i872g2">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1q2ton3_di" bpmnElement="SequenceFlow_1q2ton3">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0x127gc_di" bpmnElement="SequenceFlow_0x127gc"> <bpmndi:BPMNEdge id="SequenceFlow_0x127gc_di" bpmnElement="SequenceFlow_0x127gc">
<di:waypoint x="530" y="117" /> <di:waypoint x="530" y="117" />
<di:waypoint x="590" y="117" /> <di:waypoint x="590" y="117" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1q2ton3_di" bpmnElement="SequenceFlow_1q2ton3">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0i872g2_di" bpmnElement="SequenceFlow_0i872g2">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_0fdym05_di" bpmnElement="EndEvent_0fdym05"> <bpmndi:BPMNShape id="EndEvent_0fdym05_di" bpmnElement="EndEvent_0fdym05">
<dc:Bounds x="752" y="99" width="36" height="36" /> <dc:Bounds x="752" y="99" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0yy50p2_di" bpmnElement="SequenceFlow_0yy50p2">
<di:waypoint x="690" y="117" />
<di:waypoint x="752" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="UserTask_0li5ksb_di" bpmnElement="Task_GetWorkflow"> <bpmndi:BPMNShape id="UserTask_0li5ksb_di" bpmnElement="Task_GetWorkflow">
<dc:Bounds x="270" y="77" width="100" height="80" /> <dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0a7bvlf" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0"> <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_0a7bvlf" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.10.0">
<bpmn:process id="Process_0inkg2m" name="Start Workflow Programmatically" isExecutable="true"> <bpmn:process id="Process_0inkg2m" name="Start Workflow Programmatically" isExecutable="true">
<bpmn:startEvent id="StartEvent_1"> <bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0ac3s7d</bpmn:outgoing> <bpmn:outgoing>Flow_0ac3s7d</bpmn:outgoing>
@ -14,7 +14,7 @@
<bpmn:userTask id="Activity_0n0md5g" name="Get Required Data" camunda:formKey="DataForm"> <bpmn:userTask id="Activity_0n0md5g" name="Get Required Data" camunda:formKey="DataForm">
<bpmn:extensionElements> <bpmn:extensionElements>
<camunda:formData> <camunda:formData>
<camunda:formField id="workflow_spec_to_start" label="&#39;Workflow Spec&#39;" type="string"> <camunda:formField id="workflow_spec_to_start" label="&#39;Workflow Spec&#39;" type="string" defaultValue="&#39;random_fact&#39;">
<camunda:validation> <camunda:validation>
<camunda:constraint name="required" config="True" /> <camunda:constraint name="required" config="True" />
</camunda:validation> </camunda:validation>

View File

@ -4,7 +4,7 @@ from crc import session
from crc.models.data_store import DataStoreModel from crc.models.data_store import DataStoreModel
from crc.models.file import FileModel from crc.models.file import FileModel
from crc.models.task_event import TaskEventModel from crc.models.task_event import TaskEventModel, TaskAction
from crc.services.workflow_service import WorkflowService from crc.services.workflow_service import WorkflowService
from io import BytesIO from io import BytesIO
@ -100,7 +100,7 @@ class TestDeleteTaskData(BaseTest):
# Make sure we have something in task_events # Make sure we have something in task_events
task_events = session.query(TaskEventModel).\ task_events = session.query(TaskEventModel).\
filter(TaskEventModel.workflow_id == workflow.id).\ filter(TaskEventModel.workflow_id == workflow.id).\
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_COMPLETE).all() filter(TaskEventModel.action == TaskAction.COMPLETE.value).all()
for task_event in task_events: for task_event in task_events:
self.assertNotEqual({}, task_event.form_data) self.assertNotEqual({}, task_event.form_data)
@ -117,7 +117,7 @@ class TestDeleteTaskData(BaseTest):
files = session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all() files = session.query(FileModel).filter(FileModel.workflow_id == workflow.id).all()
task_events = session.query(TaskEventModel).\ task_events = session.query(TaskEventModel).\
filter(TaskEventModel.workflow_id == workflow.id).\ filter(TaskEventModel.workflow_id == workflow.id).\
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_COMPLETE).all() filter(TaskEventModel.action == TaskAction.COMPLETE.value).all()
self.assertEqual(0, len(data_stores)) self.assertEqual(0, len(data_stores))
self.assertEqual(0, len(data_stores_1)) self.assertEqual(0, len(data_stores_1))

View File

@ -22,6 +22,7 @@ class TestStartWorkflow(BaseTest):
return workflow return workflow
def test_start_workflow_validation(self): def test_start_workflow_validation(self):
random_wf = self.create_workflow('random_fact') # Assure we have a workflow to start.
spec_model = self.load_test_spec('start_workflow') spec_model = self.load_test_spec('start_workflow')
rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers()) rv = self.app.get('/v1.0/workflow-specification/%s/validate' % spec_model.id, headers=self.logged_in_headers())
self.assertEqual([], rv.json) self.assertEqual([], rv.json)

View File

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

View File

@ -6,7 +6,7 @@ from crc.models.api_models import NavigationItemSchema
from crc.models.workflow import WorkflowStatus from crc.models.workflow import WorkflowStatus
from crc import db from crc import db
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.task_event import TaskEventModel, TaskEventSchema from crc.models.task_event import TaskEventModel, TaskEventSchema, TaskAction
from crc.services.workflow_service import WorkflowService from crc.services.workflow_service import WorkflowService
@ -82,7 +82,7 @@ class TestUserRoles(BaseTest):
# the supervisor. # the supervisor.
task_logs = db.session.query(TaskEventModel). \ task_logs = db.session.query(TaskEventModel). \
filter(TaskEventModel.user_uid == supervisor.uid). \ filter(TaskEventModel.user_uid == supervisor.uid). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).all() filter(TaskEventModel.action == TaskAction.ASSIGNMENT.value).all()
self.assertEqual(1, len(task_logs)) self.assertEqual(1, len(task_logs))
# A call to the /task endpoint as the supervisor user should return a list of # A call to the /task endpoint as the supervisor user should return a list of
@ -213,7 +213,7 @@ class TestUserRoles(BaseTest):
def get_assignment_task_events(self, uid): def get_assignment_task_events(self, uid):
return db.session.query(TaskEventModel). \ return db.session.query(TaskEventModel). \
filter(TaskEventModel.user_uid == uid). \ filter(TaskEventModel.user_uid == uid). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).all() filter(TaskEventModel.action == TaskAction.ASSIGNMENT.value).all()
def test_workflow_reset_correctly_resets_the_task_events(self): def test_workflow_reset_correctly_resets_the_task_events(self):

View File

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

View File

@ -23,7 +23,7 @@ class TestWorkflowReset(BaseTest):
second_task = workflow_api.next_task second_task = workflow_api.next_task
self.assertEqual('Task_GetAge', second_task.name) self.assertEqual('Task_GetAge', second_task.name)
ResetWorkflow().do_task(second_task, workflow.study_id, workflow.id, reset_id='two_user_tasks') ResetWorkflow().do_task(second_task, workflow.study_id, workflow.id, workflow_spec_id='two_user_tasks')
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task task = workflow_api.next_task
@ -43,4 +43,12 @@ class TestWorkflowReset(BaseTest):
first_task = workflow_api.next_task first_task = workflow_api.next_task
with self.assertRaises(ApiError): with self.assertRaises(ApiError):
ResetWorkflow().do_task(first_task, workflow.study_id, workflow.id, reset_id='bad_workflow_name') ResetWorkflow().do_task(first_task, workflow.study_id, workflow.id, workflow_spec_id='bad_workflow_name')
def test_workflow_reset_no_start(self):
"""Sometimes we want to reset the workflow, but not start it up (don't do the engine steps etc...)"""
workflow = self.create_workflow('two_user_tasks')
workflow_api = self.get_workflow_api(workflow)
task = workflow_api.next_task
ResetWorkflow().do_task(task, workflow.study_id, workflow.id, workflow_spec_id='two_user_tasks')

View File

@ -68,7 +68,7 @@ class TestWorkflowRestart(BaseTest):
self.assertEqual(True, IsFileUploaded.do_task( self.assertEqual(True, IsFileUploaded.do_task(
IsFileUploaded, first_task, study_id, workflow.id, irb_code)) IsFileUploaded, first_task, study_id, workflow.id, irb_code))
workflow_api = self.restart_workflow_api(workflow_api, delete_files=True) workflow_api = self.restart_workflow_api(workflow_api, clear_data=True)
first_task = workflow_api.next_task first_task = workflow_api.next_task
# Assert we do not have the file # Assert we do not have the file