diff --git a/crc/api/tools.py b/crc/api/tools.py index e889f73b..64ad8a0b 100644 --- a/crc/api/tools.py +++ b/crc/api/tools.py @@ -96,11 +96,12 @@ def evaluate_python_expression(body): except Exception as e: return {"result": False, "expression": body['expression'], "key": body['key'], "error": str(e)} + def send_test_email(subject, address, message, data=None): - rendered, wrapped = EmailService().get_rendered_content(message, data) + content, content_html = EmailService().get_rendered_content(message, data) EmailService.add_email( subject=subject, sender=DEFAULT_SENDER, recipients=[address], - content=rendered, - content_html=wrapped) + content=content, + content_html=content_html) diff --git a/crc/models/email.py b/crc/models/email.py index a549cfdb..cf2bb543 100644 --- a/crc/models/email.py +++ b/crc/models/email.py @@ -23,9 +23,8 @@ class EmailModel(db.Model): class EmailModelSchema(ma.Schema): - # TODO: clean this us. Do we need load_instance and unknown? + class Meta: model = EmailModel - load_instance = True - additional = ["id", "subject", "sender", "recipients", "timestamp"] - unknown = INCLUDE + fields = ["id", "subject", "sender", "recipients", "cc", "bcc", "content", "content_html", + "study_id", "timestamp", "workflow_spec_id"] diff --git a/crc/scripts/email.py b/crc/scripts/email.py index 307cbda3..2e5a7105 100644 --- a/crc/scripts/email.py +++ b/crc/scripts/email.py @@ -3,6 +3,7 @@ import traceback from crc import app, session from crc.api.common import ApiError +from crc.models.email import EmailModelSchema from crc.models.file import FileModel, CONTENT_TYPES from crc.models.workflow import WorkflowModel from crc.services.document_service import DocumentService @@ -93,7 +94,7 @@ email(subject="My Subject", recipients="user@example.com", attachments=['Study_A print(repr(traceback.format_exception(exc_type, exc_value, exc_traceback))) raise e - return email_model.id + return EmailModelSchema().dump(email_model) def get_email_addresses(self, users, study_id): emails = [] diff --git a/crc/scripts/get_localtime.py b/crc/scripts/get_localtime.py new file mode 100644 index 00000000..83fab223 --- /dev/null +++ b/crc/scripts/get_localtime.py @@ -0,0 +1,34 @@ +from crc.api.common import ApiError +from crc.scripts.script import Script + +import dateparser +import pytz + + +class GetLocaltime(Script): + + def get_description(self): + return """Converts a UTC Datetime object into a Datetime object with a different timezone. + Defaults to US/Eastern""" + + def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): + if 'timestamp' in kwargs: + return True + raise ApiError(code='missing_timestamp', + message='You must include a timestamp to convert.') + + def do_task(self, task, study_id, workflow_id, *args, **kwargs): + if 'timestamp' in kwargs: + timestamp = kwargs['timestamp'] + if 'timezone' in kwargs: + timezone = kwargs['timezone'] + else: + timezone = 'US/Eastern' + parsed_timestamp = dateparser.parse(timestamp) + localtime = parsed_timestamp.astimezone(pytz.timezone(timezone)) + return str(localtime) + + else: + raise ApiError(code='missing_timestamp', + message='You must include a timestamp to convert.') + diff --git a/crc/services/email_service.py b/crc/services/email_service.py index 86810faa..20006827 100644 --- a/crc/services/email_service.py +++ b/crc/services/email_service.py @@ -47,9 +47,11 @@ class EmailService(object): msg.attach(file['name'], file['type'], file_data.data) mail.send(msg) + except Exception as e: app.logger.error('An exception happened in EmailService', exc_info=True) app.logger.error(str(e)) + raise e db.session.add(email_model) db.session.commit() diff --git a/migrations/versions/ba6df7e560a1_modify_email_timestamp.py b/migrations/versions/ba6df7e560a1_modify_email_timestamp.py new file mode 100644 index 00000000..58f870de --- /dev/null +++ b/migrations/versions/ba6df7e560a1_modify_email_timestamp.py @@ -0,0 +1,24 @@ +"""modify email timestamp + +Revision ID: ba6df7e560a1 +Revises: 6d8ceb1c18cb +Create Date: 2021-10-13 10:54:23.894034 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ba6df7e560a1' +down_revision = '6d8ceb1c18cb' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute("alter table email alter column timestamp type timestamp with time zone") + + +def downgrade(): + op.execute("alter table email alter column timestamp type timestamp without time zone") diff --git a/tests/data/email/email.bpmn b/tests/data/email/email.bpmn index 40489409..389ba06d 100644 --- a/tests/data/email/email.bpmn +++ b/tests/data/email/email.bpmn @@ -19,15 +19,12 @@ Email content to be delivered to {{ ApprvlApprvr1 }} --- **Test Some Formatting** - _UVA Tracking Number:_ {{ 321 }} - - + _UVA Tracking Number:_ {{ 321 }} Flow_08n2npe Flow_1xlrgne subject="Camunda Email Subject" recipients=[ApprvlApprvr1,PIComputingID] -email_id = email(subject=subject,recipients=recipients) - +email_model = email(subject=subject,recipients=recipients) diff --git a/tests/data/get_localtime/get_localtime.bpmn b/tests/data/get_localtime/get_localtime.bpmn new file mode 100644 index 00000000..43a136d5 --- /dev/null +++ b/tests/data/get_localtime/get_localtime.bpmn @@ -0,0 +1,74 @@ + + + + + Flow_0lnc9x0 + + + + + + Flow_0kgtoh1 + + + + This is my email + Flow_0lnc9x0 + Flow_0gtgzcf + email_model = email(subject='My Email Subject', recipients='user@example.com') + + + timestamp = email_model.timestamp +localtime = get_localtime(str(timestamp)) + Flow_0gtgzcf + Flow_0k1hbif + timestamp=email_model.timestamp +localtime = get_localtime(timestamp=timestamp) + + + # Timestamp +{{ timestamp }} + + +# Localtime +{{ localtime }} + Flow_0k1hbif + Flow_0kgtoh1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/emails/test_email_script.py b/tests/emails/test_email_script.py index cdc96f4e..0cb1d558 100644 --- a/tests/emails/test_email_script.py +++ b/tests/emails/test_email_script.py @@ -1,6 +1,8 @@ from tests.base_test import BaseTest from crc import mail from crc.models.email import EmailModel +import datetime +from unittest.mock import patch class TestEmailScript(BaseTest): @@ -35,8 +37,37 @@ class TestEmailScript(BaseTest): # Correct From field self.assertEqual('uvacrconnect@virginia.edu', outbox[0].sender) - db_emails = EmailModel.query.count() - self.assertEqual(db_emails, 1) + # Make sure we log the email + # Grab model for comparison below + db_emails = EmailModel.query.all() + self.assertEqual(len(db_emails), 1) + + # Check whether we get a good email model back from the script + self.assertIn('email_model', workflow_api.next_task.data) + self.assertEqual(db_emails[0].recipients, workflow_api.next_task.data['email_model']['recipients']) + self.assertEqual(db_emails[0].sender, workflow_api.next_task.data['email_model']['sender']) + self.assertEqual(db_emails[0].subject, workflow_api.next_task.data['email_model']['subject']) + + # Make sure timestamp is UTC + self.assertEqual(db_emails[0].timestamp.tzinfo, datetime.timezone.utc) + + @patch('crc.services.email_service.EmailService.add_email') + def test_email_raises_exception(self, mock_response): + self.load_example_data() + mock_response.return_value.ok = True + mock_response.side_effect = Exception("This is my exception!") + + workflow = self.create_workflow('email') + + task_data = { + 'PIComputingID': 'dhf8r@virginia.edu', + 'ApprvlApprvr1': 'lb3dp@virginia.edu' + } + task = self.get_workflow_api(workflow).next_task + + with mail.record_messages() as outbox: + with self.assertRaises(Exception) as e: + self.complete_form(workflow, task, task_data) def test_email_with_none_attachment(self): # This workflow specifically sends `attachments = None` as a parameter @@ -46,7 +77,6 @@ class TestEmailScript(BaseTest): task = workflow_api.next_task with mail.record_messages() as outbox: - workflow_api = self.complete_form(workflow, task, {'subject': 'My Test Email', 'recipients': 'user@example.com'}) task = workflow_api.next_task diff --git a/tests/emails/test_email_service.py b/tests/emails/test_email_service.py index 1cb36525..b045118f 100644 --- a/tests/emails/test_email_service.py +++ b/tests/emails/test_email_service.py @@ -3,6 +3,7 @@ from tests.base_test import BaseTest from crc import session from crc.models.email import EmailModel from crc.services.email_service import EmailService +from unittest.mock import patch class TestEmailService(BaseTest): @@ -10,7 +11,6 @@ class TestEmailService(BaseTest): def test_add_email(self): self.load_example_data() study = self.create_study() - workflow = self.create_workflow('random_fact') subject = 'Email Subject' sender = 'sender@sartography.com' @@ -42,3 +42,25 @@ class TestEmailService(BaseTest): self.assertEqual(email_model.content, content) self.assertEqual(email_model.content_html, content_html) self.assertEqual(email_model.study, None) + + @patch('crc.services.email_service.EmailService.add_email') + def test_add_email_with_error(self, mock_response): + + self.load_example_data() + mock_response.return_value.ok = True + mock_response.side_effect = Exception("This is my exception!") + + study = self.create_study() + + subject = 'Email Subject' + sender = 'sender@sartography.com' + recipients = ['recipient@sartography.com', 'back@sartography.com'] + content = 'Content for this email' + content_html = '

Hypertext Markup Language content for this email

' + + # Make sure we generate an error + with self.assertRaises(Exception) as e: + EmailService.add_email(subject=subject, sender=sender, recipients=recipients, + content=content, content_html=content_html, study_id=study.id) + # Make sure it's the error we want + self.assertEqual('This is my exception!', e.exception.args[0]) diff --git a/tests/scripts/test_get_localtime.py b/tests/scripts/test_get_localtime.py new file mode 100644 index 00000000..7e5b489b --- /dev/null +++ b/tests/scripts/test_get_localtime.py @@ -0,0 +1,17 @@ +from tests.base_test import BaseTest +from crc.scripts.get_localtime import GetLocaltime + + +class TestGetLocaltime(BaseTest): + + def test_get_localtime(self): + self.load_example_data() + + workflow = self.create_workflow('get_localtime') + workflow_api = self.get_workflow_api(workflow) + task = workflow_api.next_task + + timestamp = task.data['timestamp'] + localtime = task.data['localtime'] + + self.assertEqual(localtime, GetLocaltime().do_task(None, None, None, timestamp=timestamp))