From 909af9b8d6351f21ab3c65462ceee191a20a2afc Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Tue, 26 Jan 2021 09:19:28 -0500 Subject: [PATCH 01/15] Change HTML emails so they look like the CR-Connect website. Add logo, color, style, etc. Cleaned up email script a little. Also, email script no longer uses services.mails. It uses email_service directly now. --- crc/scripts/email.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/crc/scripts/email.py b/crc/scripts/email.py index 79f5dc5e..864f8e77 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(self, email_body): + return render_template('mail_content_template.html', email_body=email_body, base_url=request.base_url) From ecc553c4ea83a109a0df43761450ad986c99e0be Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Tue, 26 Jan 2021 09:20:16 -0500 Subject: [PATCH 02/15] Jinja templates and svg file for pretty emails --- crc/static/uva_rotunda.svg | 145 +++++++++++++++++++++++ crc/templates/mail_content_template.html | 16 +++ crc/templates/mail_main_template.html | 29 +++++ 3 files changed, 190 insertions(+) create mode 100644 crc/static/uva_rotunda.svg create mode 100644 crc/templates/mail_content_template.html create mode 100644 crc/templates/mail_main_template.html 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 @@ + + + + + + + image/svg+xml + + Artboard 1 + + + + + + + Artboard 1 + + + + + + + + + + + + + + + diff --git a/crc/templates/mail_content_template.html b/crc/templates/mail_content_template.html new file mode 100644 index 00000000..095d097f --- /dev/null +++ b/crc/templates/mail_content_template.html @@ -0,0 +1,16 @@ +{% extends "mail_main_template.html" %} +{% block head %} + {{ super() }} +{% endblock %}} +{% block header %} + {{ super() }} +{% endblock %}} +{% 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..5b7f3cb7 --- /dev/null +++ b/crc/templates/mail_main_template.html @@ -0,0 +1,29 @@ + + + + {% block head %} + + + Research Ramp-Up Toolkit + {% endblock %} + + + +
{% block content %}{% endblock %}
+ + + \ No newline at end of file From e72be4a217e45637721114e3ad62b03832901508 Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Tue, 26 Jan 2021 09:35:35 -0500 Subject: [PATCH 03/15] Needed to define SERVER_NAME so get_url worked properly. Made the port into a constant in config.default so I can import it into config.testing. Dan, I'm not sure this is the best approach. Let me know if you want something different. --- config/default.py | 3 ++- config/testing.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config/default.py b/config/default.py index 1570811c..598b4966 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 5b03cc41..518bc9d3 100644 --- a/config/testing.py +++ b/config/testing.py @@ -27,3 +27,6 @@ ADMIN_UIDS = ['dhf8r'] print('### USING TESTING CONFIG: ###') print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI) print('TESTING = ', TESTING) + +from config.default import DEFAULT_PORT +SERVER_NAME = f'localhost:{DEFAULT_PORT}' From c99a388405b416d8fd8dc951a45d874ea52aae3d Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Wed, 27 Jan 2021 09:50:29 -0500 Subject: [PATCH 04/15] We now set the display_order automatically. Users can modify order using the arrows. --- crc/api/workflow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crc/api/workflow.py b/crc/api/workflow.py index d9a0b498..9814952c 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() From 7b91fb9f61b94629b6cfd35561f61efa12fcb932 Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Wed, 27 Jan 2021 11:08:28 -0500 Subject: [PATCH 05/15] Modified test to check for display_order within workflow category. --- tests/workflow/test_workflow_spec_api.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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() From d7a3afe8f4133eb9cffd9cff485c8534856f1e46 Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Wed, 27 Jan 2021 17:12:03 -0500 Subject: [PATCH 06/15] changed the template to use the responsive html email template at https://github.com/leemunroe/responsive-html-email-template added method get_cr_connect_wrapper to render our new html email template. This takes the email text generated from the task element documentation as the email_body. --- crc/scripts/email.py | 4 +- crc/templates/mail_content_template.html | 6 - crc/templates/mail_main_template.html | 432 +++++++++++++++++++++-- 3 files changed, 413 insertions(+), 29 deletions(-) diff --git a/crc/scripts/email.py b/crc/scripts/email.py index 864f8e77..3160dc22 100644 --- a/crc/scripts/email.py +++ b/crc/scripts/email.py @@ -110,6 +110,6 @@ email ("My Subject", "dhf8r@virginia.edu", pi.email) return rendered, wrapped - # @staticmethod - def get_cr_connect_wrapper(self, email_body): + @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/templates/mail_content_template.html b/crc/templates/mail_content_template.html index 095d097f..76ea2b7a 100644 --- a/crc/templates/mail_content_template.html +++ b/crc/templates/mail_content_template.html @@ -1,10 +1,4 @@ {% extends "mail_main_template.html" %} -{% block head %} - {{ super() }} -{% endblock %}} -{% block header %} - {{ super() }} -{% endblock %}} {% block content %}
{{ email_body | safe }} diff --git a/crc/templates/mail_main_template.html b/crc/templates/mail_main_template.html index 5b7f3cb7..df0c2cfe 100644 --- a/crc/templates/mail_main_template.html +++ b/crc/templates/mail_main_template.html @@ -1,29 +1,419 @@ - {% block head %} + + + + CR-Connect Email Research Ramp-Up Toolkit - {% endblock %} - - -
{% block content %}{% endblock %}
- - - \ No newline at end of file + + + + + + + + + + + From 958d5a6281a918f1c15ba1ea1580b5c6ca5a6ed1 Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Thu, 28 Jan 2021 09:45:53 -0500 Subject: [PATCH 07/15] Possible solution for W3C issues raised by Sonarcloud --- crc/templates/mail_main_template.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crc/templates/mail_main_template.html b/crc/templates/mail_main_template.html index df0c2cfe..62e3ba97 100644 --- a/crc/templates/mail_main_template.html +++ b/crc/templates/mail_main_template.html @@ -355,17 +355,19 @@
- +
- +
+ + From 83632c64def5c0c5c8b75f1a9c7cc9c340a2aebc Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Thu, 28 Jan 2021 10:42:15 -0500 Subject: [PATCH 08/15] added exclusion for email template in sonarcloud properties file. --- .sonarcloud.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7e76639cf3cf6e9bef0c8ea6f7d0b6a8e010be2a Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Fri, 29 Jan 2021 14:05:07 -0500 Subject: [PATCH 09/15] When a study is put on hold, we now reset workflows and call any pending cancel_notify events. In api.study.update_study we test the study status and call the new WorkflowService method process_workflows_for_cancels. In services.workflow_service we added the new method process_workflows_for_cancels. It loops through workflows for a study, and resets them if they are in progress. In services.workflow_processor, we changed the reset method to be an instance method so we can call self.cancel_notify. In tests.test_lookup_service we changed the call to WorkflowProcessor.reset to reflect the change from class method to instance method --- crc/api/study.py | 5 +++++ crc/services/workflow_processor.py | 6 +++--- crc/services/workflow_service.py | 9 +++++++-- tests/test_lookup_service.py | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) 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/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..06d2cf3a 100644 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -704,5 +704,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/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) From c026717b4a9b1a768b3c0dc15b9a1631f98f111a Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Fri, 29 Jan 2021 14:06:25 -0500 Subject: [PATCH 10/15] Changed the call to WorkflowProcessor.reset to reflect the change from class method to instance method --- tests/workflow/test_workflow_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From f9e4b6a972b299c61c3726432d23e1734dc419da Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Fri, 29 Jan 2021 14:09:42 -0500 Subject: [PATCH 11/15] Test and workflow for the new code. We test the correct cancel notify is called, and only when appropriate. --- .../study_cancellations.bpmn | 148 ++++++++++++++++++ tests/study/test_study_cancellations.py | 107 +++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 tests/data/study_cancellations/study_cancellations.bpmn create mode 100644 tests/study/test_study_cancellations.py 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) From c5cf3ea7ffef335a3f7ec5c420cbe8b4928ade13 Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Fri, 29 Jan 2021 14:34:09 -0500 Subject: [PATCH 12/15] Changed call to WorkflowProcessor.reset to reflect change from class method to instance method. --- crc/api/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crc/api/workflow.py b/crc/api/workflow.py index d9a0b498..a5cc44be 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -115,7 +115,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) From 6f7abc0a97614ca8d2be7555dc97bc8af6cc9b6c Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Fri, 29 Jan 2021 14:48:35 -0500 Subject: [PATCH 13/15] Added tests to test_workflow_restart to make sure we call cancel_notify events when appropriate. --- tests/workflow/test_workflow_restart.py | 52 ++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) 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) + From b090e31e004637ca8c3751ca35af1afc8f7a796c Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Wed, 3 Feb 2021 09:50:16 -0500 Subject: [PATCH 14/15] This fixes 2 issues with setting boolean defaults - If the default was False, we failed an early test and returned None. We now only return None if default is None - We have a better test for deciding if the default value we return should be True or False. We used to return True if the value was the string 'false'. --- crc/services/workflow_service.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index dce8118f..e148d039 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 From 1247744463f6628149ab41237111f84a72b4a718 Mon Sep 17 00:00:00 2001 From: mike cullerton Date: Wed, 3 Feb 2021 09:50:37 -0500 Subject: [PATCH 15/15] Test and workflow for new code. --- .../boolean_default_value.bpmn | 89 +++++++++++++++++++ .../workflow/test_workflow_boolean_default.py | 71 +++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 tests/data/boolean_default_value/boolean_default_value.bpmn create mode 100644 tests/workflow/test_workflow_boolean_default.py 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/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'])
Research Ramp-Up Toolkit Configurator