diff --git a/.sonarcloud.properties b/.sonarcloud.properties
index 4394a1bb..f1205436 100644
--- a/.sonarcloud.properties
+++ b/.sonarcloud.properties
@@ -1,7 +1,7 @@
sonar.organization=sartography
sonar.projectKey=sartography_cr-connect-workflow
sonar.host.url=https://sonarcloud.io
-sonar.exclusions=docs/**,config/**,instance/**,migrations/**,postgres/**,readme_images/**,schema/**,templates/**
+sonar.exclusions=crc/templates/*.html,docs/**,config/**,instance/**,migrations/**,postgres/**,readme_images/**,schema/**,templates/**
sonar.sources=crc
sonar.test.inclusions=tests
sonar.python.coverage.reportPaths=coverage.xml
diff --git a/config/default.py b/config/default.py
index f03af5b1..0aa4e254 100644
--- a/config/default.py
+++ b/config/default.py
@@ -15,7 +15,8 @@ JSON_SORT_KEYS = False # CRITICAL. Do not sort the data when returning values
API_TOKEN = environ.get('API_TOKEN', default = 'af95596f327c9ecc007b60414fc84b61')
NAME = "CR Connect Workflow"
-FLASK_PORT = environ.get('PORT0') or environ.get('FLASK_PORT', default="5000")
+DEFAULT_PORT = "5000"
+FLASK_PORT = environ.get('PORT0') or environ.get('FLASK_PORT', default=DEFAULT_PORT)
CORS_ALLOW_ORIGINS = re.split(r',\s*', environ.get('CORS_ALLOW_ORIGINS', default="localhost:4200, localhost:5002"))
TESTING = environ.get('TESTING', default="false") == "true"
PRODUCTION = (environ.get('PRODUCTION', default="false") == "true")
diff --git a/config/testing.py b/config/testing.py
index b8375d9a..047c45df 100644
--- a/config/testing.py
+++ b/config/testing.py
@@ -31,3 +31,5 @@ print('TESTING = ', TESTING)
#Use the mock ldap.
LDAP_URL = 'mock'
+from config.default import DEFAULT_PORT
+SERVER_NAME = f'localhost:{DEFAULT_PORT}'
diff --git a/crc/api/study.py b/crc/api/study.py
index e6e1ed20..b7e0d3b0 100644
--- a/crc/api/study.py
+++ b/crc/api/study.py
@@ -9,6 +9,7 @@ from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.study import Study, StudyEvent, StudyEventType, StudyModel, StudySchema, StudyForUpdateSchema, StudyStatus
from crc.services.study_service import StudyService
from crc.services.user_service import UserService
+from crc.services.workflow_service import WorkflowService
def add_study(body):
@@ -63,6 +64,10 @@ def update_study(study_id, body):
session.add(study_model)
session.commit()
+
+ if status == StudyStatus.abandoned or status == StudyStatus.hold:
+ WorkflowService.process_workflows_for_cancels(study_id)
+
# Need to reload the full study to return it to the frontend
study = StudyService.get_study(study_id)
return StudySchema().dump(study)
diff --git a/crc/api/workflow.py b/crc/api/workflow.py
index d9a0b498..f2cb39d9 100644
--- a/crc/api/workflow.py
+++ b/crc/api/workflow.py
@@ -24,6 +24,8 @@ def all_specifications():
def add_workflow_specification(body):
+ count = session.query(WorkflowSpecModel).filter_by(category_id=body['category_id']).count()
+ body['display_order'] = count
new_spec: WorkflowSpecModel = WorkflowSpecModelSchema().load(body, session=session)
session.add(new_spec)
session.commit()
@@ -115,7 +117,7 @@ def restart_workflow(workflow_id, clear_data=False):
"""Restart a workflow with the latest spec.
Clear data allows user to restart the workflow without previous data."""
workflow_model: WorkflowModel = session.query(WorkflowModel).filter_by(id=workflow_id).first()
- WorkflowProcessor.reset(workflow_model, clear_data=clear_data)
+ WorkflowProcessor(workflow_model).reset(workflow_model, clear_data=clear_data)
return get_workflow(workflow_model.id)
diff --git a/crc/scripts/email.py b/crc/scripts/email.py
index 79f5dc5e..3160dc22 100644
--- a/crc/scripts/email.py
+++ b/crc/scripts/email.py
@@ -7,7 +7,9 @@ from crc import app
from crc.api.common import ApiError
from crc.scripts.script import Script
from crc.services.ldap_service import LdapService
-from crc.services.mails import send_mail
+from crc.services.email_service import EmailService
+
+from flask import render_template, request
class Email(Script):
@@ -25,25 +27,27 @@ email ("My Subject", "dhf8r@virginia.edu", pi.email)
"""
def do_task_validate_only(self, task, *args, **kwargs):
- self.get_subject(task, args)
+ self.get_subject(args)
self.get_email_recipients(task, args)
self.get_content(task)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
- if len(args) < 1:
+ if len(args) < 2:
raise ApiError(code="missing_argument",
message="Email script requires a subject and at least one email address as arguments")
- subject = args[0]
+ subject = self.get_subject(args)
recipients = self.get_email_recipients(task, args)
- content, content_html = self.get_content(task)
+
if recipients:
- send_mail(
+ content, content_html = self.get_content(task)
+ EmailService.add_email(
subject=subject,
sender=app.config['DEFAULT_SENDER'],
recipients=recipients,
content=content,
- content_html=content_html
+ content_html=content_html,
+ study_id=study_id
)
def check_valid_email(self, email):
@@ -82,7 +86,8 @@ email ("My Subject", "dhf8r@virginia.edu", pi.email)
return emails
- def get_subject(self, task, args):
+ @staticmethod
+ def get_subject(args):
# subject = ''
if len(args[0]) < 1:
raise ApiError(code="missing_argument",
@@ -101,4 +106,10 @@ email ("My Subject", "dhf8r@virginia.edu", pi.email)
template = Template(content)
rendered = template.render(task.data)
rendered_markdown = markdown.markdown(rendered).replace('\n', '
')
- return rendered, rendered_markdown
+ wrapped = self.get_cr_connect_wrapper(rendered_markdown)
+
+ return rendered, wrapped
+
+ @staticmethod
+ def get_cr_connect_wrapper(email_body):
+ return render_template('mail_content_template.html', email_body=email_body, base_url=request.base_url)
diff --git a/crc/services/workflow_processor.py b/crc/services/workflow_processor.py
index 5149fa97..92e8d68b 100644
--- a/crc/services/workflow_processor.py
+++ b/crc/services/workflow_processor.py
@@ -196,10 +196,10 @@ class WorkflowProcessor(object):
else:
self.is_latest_spec = False
- @classmethod
- def reset(cls, workflow_model, clear_data=False):
+ def reset(self, workflow_model, clear_data=False):
print('WorkflowProcessor: reset: ')
+ self.cancel_notify()
workflow_model.bpmn_workflow_json = None
if clear_data:
# Clear form_data from task_events
@@ -209,7 +209,7 @@ class WorkflowProcessor(object):
task_event.form_data = {}
session.add(task_event)
session.commit()
- return cls(workflow_model)
+ return self.__init__(workflow_model)
def __get_bpmn_workflow(self, workflow_model: WorkflowModel, spec: WorkflowSpec, validate_only=False):
if workflow_model.bpmn_workflow_json:
diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py
index dce8118f..74d070c0 100644
--- a/crc/services/workflow_service.py
+++ b/crc/services/workflow_service.py
@@ -251,7 +251,9 @@ class WorkflowService(object):
default = result
# If no default exists, return None
- if not default: return None
+ # Note: if default is False, we don't want to execute this code
+ if default is None:
+ return None
if field.type == "enum" and not has_lookup:
default_option = next((obj for obj in field.options if obj.id == default), None)
@@ -278,7 +280,10 @@ class WorkflowService(object):
elif field.type == "long":
return int(default)
elif field.type == 'boolean':
- return bool(default)
+ default = str(default).lower()
+ if default == 'true' or default == 't':
+ return True
+ return False
else:
return default
@@ -704,5 +709,10 @@ class WorkflowService(object):
return data
-
-
+ @staticmethod
+ def process_workflows_for_cancels(study_id):
+ workflows = db.session.query(WorkflowModel).filter_by(study_id=study_id).all()
+ for workflow in workflows:
+ if workflow.status == WorkflowStatus.user_input_required or workflow.status == WorkflowStatus.waiting:
+ processor = WorkflowProcessor(workflow)
+ processor.reset(workflow)
diff --git a/crc/static/uva_rotunda.svg b/crc/static/uva_rotunda.svg
new file mode 100644
index 00000000..6ef09251
--- /dev/null
+++ b/crc/static/uva_rotunda.svg
@@ -0,0 +1,145 @@
+
+
diff --git a/crc/templates/mail_content_template.html b/crc/templates/mail_content_template.html
new file mode 100644
index 00000000..76ea2b7a
--- /dev/null
+++ b/crc/templates/mail_content_template.html
@@ -0,0 +1,10 @@
+{% extends "mail_main_template.html" %}
+{% block content %}
+
+ {{ email_body | safe }}
+
+{% endblock %}
+{% block footer %}
+ {{ super() }}
+ Ramp-Up Toolkit Configurator - University of Virginia
+{% endblock %}}
diff --git a/crc/templates/mail_main_template.html b/crc/templates/mail_main_template.html
new file mode 100644
index 00000000..62e3ba97
--- /dev/null
+++ b/crc/templates/mail_main_template.html
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+
+ CR-Connect Email
+
+ Research Ramp-Up Toolkit
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}{% endblock %}
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
diff --git a/tests/data/boolean_default_value/boolean_default_value.bpmn b/tests/data/boolean_default_value/boolean_default_value.bpmn
new file mode 100644
index 00000000..4f820e65
--- /dev/null
+++ b/tests/data/boolean_default_value/boolean_default_value.bpmn
@@ -0,0 +1,89 @@
+
+
+
+
+ Flow_1x41riu
+
+
+
+
+
+
+
+
+
+
+
+ Flow_0zp5mss
+ Flow_0m31ypa
+
+
+
+ <H1>Good Bye</H1>
+<div><span>Pick One: {% if pick_one %}{{ pick_one}}{% endif %} </span></div>
+
+ Flow_0m31ypa
+ Flow_0f3gndz
+
+
+ Flow_0f3gndz
+
+
+
+
+ <H1>Hello</H1>
+ Flow_1x41riu
+ Flow_1i32jb7
+
+
+
+
+ Flow_1i32jb7
+ Flow_0zp5mss
+ if not 'yes_no' in globals():
+ yes_no = False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/data/study_cancellations/study_cancellations.bpmn b/tests/data/study_cancellations/study_cancellations.bpmn
new file mode 100644
index 00000000..b164ce19
--- /dev/null
+++ b/tests/data/study_cancellations/study_cancellations.bpmn
@@ -0,0 +1,148 @@
+
+
+
+
+ Flow_0xym55y
+
+
+
+ Flow_16q1uec
+ update_study("title:'New Title'")
+print('New Title')
+
+
+
+
+
+
+
+
+ Flow_1e9j7mj
+ Flow_07i0gvv
+
+
+
+ Flow_16q1uec
+
+
+
+ <H1>Hello</H1>
+ Flow_0xym55y
+ Flow_1e9j7mj
+
+
+
+ Flow_0rus4fi
+
+
+
+ <H1>Good Bye</H1>
+ Flow_0f79pbo
+ Flow_0rus4fi
+
+
+
+
+
+
+
+ Flow_07i0gvv
+ Flow_0f79pbo
+
+
+
+ Flow_13xidv2
+
+
+
+
+ <H1>Cancel Message</H1>
+ Flow_13xidv2
+ update_study("title:'Second Title'")
+print('Second Title')
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/study/test_study_cancellations.py b/tests/study/test_study_cancellations.py
new file mode 100644
index 00000000..4b0d61f9
--- /dev/null
+++ b/tests/study/test_study_cancellations.py
@@ -0,0 +1,107 @@
+from tests.base_test import BaseTest
+
+from crc import session
+from crc.models.study import StudyModel, StudySchema
+from crc.models.workflow import WorkflowModel, WorkflowSpecModel
+
+import json
+
+
+class TestStudyCancellations(BaseTest):
+
+ def update_study_status(self, study, study_schema):
+ put_response = self.app.put('/v1.0/study/%i' % study.id,
+ content_type="application/json",
+ headers=self.logged_in_headers(),
+ data=json.dumps(study_schema))
+ self.assert_success(put_response)
+
+ # The error happened when the dashboard reloaded,
+ # in particular, when we got the studies for the user
+ api_response = self.app.get('/v1.0/study', headers=self.logged_in_headers(), content_type="application/json")
+ self.assert_success(api_response)
+
+ study_result = session.query(StudyModel).filter(StudyModel.id == study.id).first()
+ return study_result
+
+ def put_study_on_hold(self, study_id):
+ study = session.query(StudyModel).filter_by(id=study_id).first()
+
+ study_schema = StudySchema().dump(study)
+ study_schema['status'] = 'hold'
+ study_schema['comment'] = 'This is my hold comment'
+
+ self.update_study_status(study, study_schema)
+
+ study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first()
+ return study_result
+
+ def load_workflow(self):
+ self.load_example_data()
+ workflow = self.create_workflow('study_cancellations')
+ study_id = workflow.study_id
+ return workflow, study_id
+
+ def get_first_task(self, workflow):
+ workflow_api = self.get_workflow_api(workflow)
+ first_task = workflow_api.next_task
+ self.assertEqual('Activity_Hello', first_task.name)
+ return workflow_api, first_task
+
+ def get_second_task(self, workflow):
+ workflow_api = self.get_workflow_api(workflow)
+ second_task = workflow_api.next_task
+ self.assertEqual('Activity_HowMany', second_task.name)
+ return workflow_api, second_task
+
+ def get_third_task(self, workflow):
+ workflow_api = self.get_workflow_api(workflow)
+ third_task = workflow_api.next_task
+ self.assertEqual('Activity_Modify', third_task.name)
+ return workflow_api, third_task
+
+ def test_before_cancel(self):
+
+ workflow, study_id = self.load_workflow()
+ self.get_first_task(workflow)
+
+ study_result = self.put_study_on_hold(study_id)
+ self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title)
+
+ def test_first_cancel(self):
+ workflow, study_id = self.load_workflow()
+ workflow_api, first_task = self.get_first_task(workflow)
+
+ self.complete_form(workflow_api, first_task, {})
+
+ study_result = self.put_study_on_hold(study_id)
+ self.assertEqual('New Title', study_result.title)
+
+ def test_second_cancel(self):
+
+ workflow, study_id = self.load_workflow()
+ workflow_api, first_task = self.get_first_task(workflow)
+
+ self.complete_form(workflow_api, first_task, {})
+
+ workflow_api, next_task = self.get_second_task(workflow)
+ self.complete_form(workflow_api, next_task, {'how_many': 3})
+
+ study_result = self.put_study_on_hold(study_id)
+ self.assertEqual('Second Title', study_result.title)
+
+ def test_after_cancel(self):
+
+ workflow, study_id = self.load_workflow()
+ workflow_api, first_task = self.get_first_task(workflow)
+
+ self.complete_form(workflow_api, first_task, {})
+
+ workflow_api, second_task = self.get_second_task(workflow)
+ self.complete_form(workflow_api, second_task, {'how_many': 3})
+
+ workflow_api, third_task = self.get_third_task(workflow)
+ self.complete_form(workflow_api, third_task, {})
+
+ study_result = self.put_study_on_hold(study_id)
+ self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title)
diff --git a/tests/test_lookup_service.py b/tests/test_lookup_service.py
index e9da7fa8..1781e2ae 100644
--- a/tests/test_lookup_service.py
+++ b/tests/test_lookup_service.py
@@ -54,7 +54,8 @@ class TestLookupService(BaseTest):
# restart the workflow, so it can pick up the changes.
- processor = WorkflowProcessor.reset(workflow)
+ processor = WorkflowProcessor(workflow)
+ processor.reset(workflow)
workflow = processor.workflow_model
LookupService.lookup(workflow, "sponsor", "sam", limit=10)
diff --git a/tests/workflow/test_workflow_boolean_default.py b/tests/workflow/test_workflow_boolean_default.py
new file mode 100644
index 00000000..bd97d2ec
--- /dev/null
+++ b/tests/workflow/test_workflow_boolean_default.py
@@ -0,0 +1,71 @@
+from tests.base_test import BaseTest
+
+
+class TestBooleanDefault(BaseTest):
+
+ def do_test(self, yes_no):
+ workflow = self.create_workflow('boolean_default_value')
+ workflow_api = self.get_workflow_api(workflow)
+ first_task = workflow_api.next_task
+ result = self.complete_form(workflow_api, first_task, {'yes_no': yes_no})
+ return result
+
+ def test_boolean_true_string(self):
+
+ yes_no = 'True'
+ result = self.do_test(yes_no)
+ self.assertEqual(True, result.next_task.data['pick_one'])
+
+ def test_boolean_true_string_lower(self):
+
+ yes_no = 'true'
+ result = self.do_test(yes_no)
+ self.assertEqual(True, result.next_task.data['pick_one'])
+
+ def test_boolean_t_string(self):
+
+ yes_no = 'T'
+ result = self.do_test(yes_no)
+ self.assertEqual(True, result.next_task.data['pick_one'])
+
+ def test_boolean_t_string_lower(self):
+
+ yes_no = 't'
+ result = self.do_test(yes_no)
+ self.assertEqual(True, result.next_task.data['pick_one'])
+
+ def test_boolean_true(self):
+
+ yes_no = True
+ result = self.do_test(yes_no)
+ self.assertEqual(True, result.next_task.data['pick_one'])
+
+ def test_boolean_false_string(self):
+
+ yes_no = 'False'
+ result = self.do_test(yes_no)
+ self.assertEqual(False, result.next_task.data['pick_one'])
+
+ def test_boolean_false_string_lower(self):
+
+ yes_no = 'false'
+ result = self.do_test(yes_no)
+ self.assertEqual(False, result.next_task.data['pick_one'])
+
+ def test_boolean_f_string(self):
+
+ yes_no = 'F'
+ result = self.do_test(yes_no)
+ self.assertEqual(False, result.next_task.data['pick_one'])
+
+ def test_boolean_f_string_lower(self):
+
+ yes_no = 'f'
+ result = self.do_test(yes_no)
+ self.assertEqual(False, result.next_task.data['pick_one'])
+
+ def test_boolean_false(self):
+
+ yes_no = False
+ result = self.do_test(yes_no)
+ self.assertEqual(False, result.next_task.data['pick_one'])
diff --git a/tests/workflow/test_workflow_processor.py b/tests/workflow/test_workflow_processor.py
index 843a4f1f..21d9ba48 100644
--- a/tests/workflow/test_workflow_processor.py
+++ b/tests/workflow/test_workflow_processor.py
@@ -279,7 +279,7 @@ class TestWorkflowProcessor(BaseTest):
self.assertFalse(processor2.is_latest_spec) # Still at version 1.
# Do a hard reset, which should bring us back to the beginning, but retain the data.
- WorkflowProcessor.reset(processor2.workflow_model)
+ processor2.reset(processor2.workflow_model)
processor3 = WorkflowProcessor(processor.workflow_model)
processor3.do_engine_steps()
self.assertEqual("Step 1", processor3.next_task().task_spec.description)
diff --git a/tests/workflow/test_workflow_restart.py b/tests/workflow/test_workflow_restart.py
index dddbffeb..98688964 100644
--- a/tests/workflow/test_workflow_restart.py
+++ b/tests/workflow/test_workflow_restart.py
@@ -1,9 +1,11 @@
from tests.base_test import BaseTest
+from crc import session
+from crc.models.study import StudyModel
-class TestMessageEvent(BaseTest):
+class TestWorkflowRestart(BaseTest):
- def test_message_event(self):
+ def test_workflow_restart(self):
workflow = self.create_workflow('message_event')
@@ -32,3 +34,49 @@ class TestMessageEvent(BaseTest):
self.assertNotIn('formdata', workflow_api.next_task.data)
print('Nice Test')
+
+ def test_workflow_restart_on_cancel_notify(self):
+ workflow = self.create_workflow('message_event')
+ study_id = workflow.study_id
+
+ # Start the workflow.
+ first_task = self.get_workflow_api(workflow).next_task
+ self.assertEqual('Activity_GetData', first_task.name)
+ workflow_api = self.get_workflow_api(workflow)
+ self.complete_form(workflow_api, first_task, {'formdata': 'asdf'})
+ workflow_api = self.get_workflow_api(workflow)
+ self.assertEqual('Activity_HowMany', workflow_api.next_task.name)
+
+ workflow_api = self.restart_workflow_api(workflow)
+ study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first()
+ self.assertEqual('New Title', study_result.title)
+
+ def test_workflow_restart_before_cancel_notify(self):
+ workflow = self.create_workflow('message_event')
+ study_id = workflow.study_id
+
+ first_task = self.get_workflow_api(workflow).next_task
+ self.assertEqual('Activity_GetData', first_task.name)
+
+ study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first()
+ self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title)
+
+ def test_workflow_restart_after_cancel_notify(self):
+ workflow = self.create_workflow('message_event')
+ study_id = workflow.study_id
+
+ # Start the workflow.
+ first_task = self.get_workflow_api(workflow).next_task
+ self.assertEqual('Activity_GetData', first_task.name)
+ workflow_api = self.get_workflow_api(workflow)
+ self.complete_form(workflow_api, first_task, {'formdata': 'asdf'})
+
+ workflow_api = self.get_workflow_api(workflow)
+ next_task = workflow_api.next_task
+ self.assertEqual('Activity_HowMany', next_task.name)
+ self.complete_form(workflow_api, next_task, {'how_many': 3})
+
+ workflow_api = self.restart_workflow_api(workflow)
+ study_result = session.query(StudyModel).filter(StudyModel.id == study_id).first()
+ self.assertEqual('Beer consumption in the bipedal software engineer', study_result.title)
+
diff --git a/tests/workflow/test_workflow_spec_api.py b/tests/workflow/test_workflow_spec_api.py
index 5681270c..d54fbbf1 100644
--- a/tests/workflow/test_workflow_spec_api.py
+++ b/tests/workflow/test_workflow_spec_api.py
@@ -25,8 +25,10 @@ class TestWorkflowSpec(BaseTest):
def test_add_new_workflow_specification(self):
self.load_example_data()
num_before = session.query(WorkflowSpecModel).count()
+ category_id = session.query(WorkflowSpecCategoryModel).first().id
+ category_count = session.query(WorkflowSpecModel).filter_by(category_id=category_id).count()
spec = WorkflowSpecModel(id='make_cookies', name='make_cookies', display_name='Cooooookies',
- description='Om nom nom delicious cookies')
+ description='Om nom nom delicious cookies', category_id=category_id)
rv = self.app.post('/v1.0/workflow-specification',
headers=self.logged_in_headers(),
content_type="application/json",
@@ -36,6 +38,9 @@ class TestWorkflowSpec(BaseTest):
self.assertEqual(spec.display_name, db_spec.display_name)
num_after = session.query(WorkflowSpecModel).count()
self.assertEqual(num_after, num_before + 1)
+ self.assertEqual(category_count, db_spec.display_order)
+ category_count_after = session.query(WorkflowSpecModel).filter_by(category_id=category_id).count()
+ self.assertEqual(category_count_after, category_count + 1)
def test_get_workflow_specification(self):
self.load_example_data()